Skip to content

Commit 388d5db

Browse files
author
Michael Ng
authored
feat(decision): Hook up the different pieces of rollouts. (#45)
1 parent bad9b0b commit 388d5db

17 files changed

+248
-64
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,41 @@
11
# Optimizely Go SDK
22

3+
## Usage
4+
5+
### Instantiation
6+
To start using the SDK, create an instance using our factory method:
7+
8+
```
9+
import "github.com/optimizely/go-sdk/optimizely/client"
10+
11+
optimizelyFactory := &client.OptimizelyFactory{
12+
SDKKey: "[SDK_KEY_HERE]",
13+
}
14+
15+
client, err := optimizelyFactory.Client()
16+
17+
// You can also instantiate with a hard-coded datafile
18+
optimizelyFactory := &client.OptimizelyFactory{
19+
Datafile: []byte("datafile_string"),
20+
}
21+
22+
client, err := optimizelyFactory.Client()
23+
24+
```
25+
26+
### Feature Rollouts
27+
```
28+
user := entities.UserContext{
29+
ID: "optimizely end user",
30+
Attributes: entities.UserAttributes{
31+
"state": "California",
32+
"likes_donuts": true,
33+
},
34+
}
35+
36+
enabled, _ := client.IsFeatureEnabled("binary_feature", user)
37+
```
38+
339
## Command line interface
440
A CLI has been provided to illustrate the functionality of the SDK. Simply run `go-sdk` for help.
541
```$sh

examples/main.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@ package main
22

33
import (
44
"fmt"
5-
65
"time"
76

87
"github.com/optimizely/go-sdk/optimizely/client"
98
"github.com/optimizely/go-sdk/optimizely/entities"
109
"github.com/optimizely/go-sdk/optimizely/event"
10+
"github.com/optimizely/go-sdk/optimizely/logging"
1111
)
1212

1313
func main() {
14+
logging.SetLogLevel(logging.LogLevelDebug)
1415
optimizelyFactory := &client.OptimizelyFactory{
15-
SDKKey: "ABC",
16+
SDKKey: "4SLpaJA1r1pgE6T2CoMs9q",
17+
Datafile: []byte("datafile_string"),
1618
}
1719
client, err := optimizelyFactory.Client()
1820

@@ -29,7 +31,7 @@ func main() {
2931
},
3032
}
3133

32-
enabled, _ := client.IsFeatureEnabled("go_sdk", user)
34+
enabled, _ := client.IsFeatureEnabled("binary_feature", user)
3335
fmt.Printf("Is feature enabled? %v", enabled)
3436

3537
processor := event.NewEventProcessor(100, 100)

optimizely/client/client.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package client
1818

1919
import (
2020
"errors"
21+
"fmt"
2122

2223
"github.com/optimizely/go-sdk/optimizely"
2324
"github.com/optimizely/go-sdk/optimizely/decision"
@@ -54,12 +55,22 @@ func (o *OptimizelyClient) IsFeatureEnabled(featureKey string, userContext entit
5455
ProjectConfig: projectConfig,
5556
}
5657

58+
userID := userContext.ID
59+
logger.Debug(fmt.Sprintf(`Evaluating feature "%s" for user "%s".`, featureKey, userID))
5760
featureDecision, err := o.decisionService.GetFeatureDecision(featureDecisionContext, userContext)
5861
if err != nil {
5962
logger.Error("Received an error while computing feature decision", err)
6063
return false, err
6164
}
6265

66+
logger.Debug(fmt.Sprintf(`Decision made for feature "%s" for user "%s" with the following reason: "%s". Source: "%s".`, featureKey, userID, featureDecision.Reason, featureDecision.Source))
67+
68+
if featureDecision.Variation.FeatureEnabled == true {
69+
logger.Info(fmt.Sprintf(`Feature "%s" is enabled for user "%s".`, featureKey, userID))
70+
} else {
71+
logger.Info(fmt.Sprintf(`Feature "%s" is not enabled for user "%s".`, featureKey, userID))
72+
}
73+
6374
// @TODO(mng): send impression event
6475
return featureDecision.Variation.FeatureEnabled, nil
6576
}

optimizely/config/datafileProjectConfig/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func NewDatafileProjectConfig(jsonDatafile []byte) (*DatafileProjectConfig, erro
7979
experiments, experimentKeyMap := mappers.MapExperiments(datafile.Experiments)
8080
rolloutMap := mappers.MapRollouts(datafile.Rollouts)
8181
config := &DatafileProjectConfig{
82-
audienceMap: mappers.MapAudiences(datafile.Audiences),
82+
audienceMap: mappers.MapAudiences(datafile.TypedAudiences),
8383
experimentMap: experiments,
8484
experimentKeyToIDMap: experimentKeyMap,
8585
rolloutMap: rolloutMap,

optimizely/config/datafileProjectConfig/entities/entities.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ package entities
2020
type Audience struct {
2121
ID string `json:"id"`
2222
Name string `json:"name"`
23-
Conditions interface{} `json:"condition"`
23+
Conditions interface{} `json:"conditions"`
24+
}
25+
26+
// Attribute represents an Attribute object from the Optimizely datafile
27+
type Attribute struct {
28+
ID string `json:"id"`
29+
Key string `json:"key"`
2430
}
2531

2632
// Experiment represents an Experiment object from the Optimizely datafile
@@ -38,11 +44,19 @@ type Experiment struct {
3844

3945
// FeatureFlag represents a FeatureFlag object from the Optimizely datafile
4046
type FeatureFlag struct {
41-
ID string `json:"id"`
42-
RolloutID string `json:"rolloutId"`
43-
Key string `json:"key"`
44-
ExperimentIDs []string `json:"experimentIds"`
45-
Variables []string `json:"variables"`
47+
ID string `json:"id"`
48+
RolloutID string `json:"rolloutId"`
49+
Key string `json:"key"`
50+
ExperimentIDs []string `json:"experimentIds"`
51+
Variables []Variable `json:"variables"`
52+
}
53+
54+
// Variable represents a Variable object from the Optimizely datafile
55+
type Variable struct {
56+
DefaultValue string `json:"defaultValue"`
57+
ID string `json:"id"`
58+
Key string `json:"key"`
59+
Type string `json:"type"`
4660
}
4761

4862
// trafficAllocation represents a traffic allocation range from the Optimizely datafile
@@ -76,6 +90,7 @@ type Rollout struct {
7690
type Datafile struct {
7791
AccountID string `json:"accountId"`
7892
AnonymizeIP bool `json:"anonymizeIP"`
93+
Attributes []Attribute `json:"attributes"`
7994
Audiences []Audience `json:"audiences"`
8095
BotFiltering bool `json:"botFiltering"`
8196
Experiments []Experiment `json:"experiments"`

optimizely/config/datafileProjectConfig/mappers/condition_trees.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ package mappers
1919
import (
2020
"encoding/json"
2121
"errors"
22-
"github.com/optimizely/go-sdk/optimizely/entities"
2322
"reflect"
23+
24+
"github.com/optimizely/go-sdk/optimizely/entities"
2425
)
2526

2627
var ErrEmptyTree = errors.New("Empty Tree")
2728

2829
// Takes the conditions array from the audience in the datafile and turns it into a condition tree
2930
func buildConditionTree(conditions interface{}) (conditionTree *entities.TreeNode, retErr error) {
30-
3131
value := reflect.ValueOf(conditions)
3232
visited := make(map[interface{}]bool)
3333

optimizely/decision/bucketer/experiment_bucketer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func (b MurmurhashBucketer) Bucket(bucketingID string, experiment entities.Exper
5555
return variation, reasons.BucketedIntoVariation, nil
5656
}
5757

58-
return entities.Variation{}, reasons.BucketedVariationNotFound, nil
58+
return entities.Variation{ID: bucketedVariationID}, reasons.BucketedVariationNotFound, nil
5959
}
6060

6161
func (b MurmurhashBucketer) bucketToEntity(bucketKey string, trafficAllocations []entities.Range) (entityID string) {

optimizely/decision/composite_feature_service.go

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,40 +22,44 @@ import (
2222

2323
// CompositeFeatureService is the default out-of-the-box feature decision service
2424
type CompositeFeatureService struct {
25-
experimentDecisionService ExperimentDecisionService
26-
rolloutExperimentDecisionService ExperimentDecisionService
25+
experimentDecisionService ExperimentDecisionService
26+
rolloutDecisionService FeatureDecisionService
2727
}
2828

2929
// NewCompositeFeatureService returns a new instance of the CompositeFeatureService
30-
func NewCompositeFeatureService(experimentDecisionService ExperimentDecisionService) *CompositeFeatureService {
31-
if experimentDecisionService == nil {
32-
experimentDecisionService = NewCompositeExperimentService()
33-
}
30+
func NewCompositeFeatureService() *CompositeFeatureService {
3431
return &CompositeFeatureService{
35-
experimentDecisionService: experimentDecisionService,
32+
rolloutDecisionService: NewRolloutService(),
3633
}
3734
}
3835

3936
// GetDecision returns a decision for the given feature and user context
40-
func (featureService CompositeFeatureService) GetDecision(decisionContext FeatureDecisionContext, userContext entities.UserContext) (FeatureDecision, error) {
41-
featureDecision := FeatureDecision{}
37+
func (f CompositeFeatureService) GetDecision(decisionContext FeatureDecisionContext, userContext entities.UserContext) (FeatureDecision, error) {
4238
feature := decisionContext.Feature
4339

4440
// Check if user is bucketed in feature experiment
45-
// @TODO: add in a feature decision service that takes into account multiple experiments (via group mutex)
46-
experiment := feature.FeatureExperiments[0]
47-
experimentDecisionContext := ExperimentDecisionContext{
48-
Experiment: &experiment,
49-
ProjectConfig: decisionContext.ProjectConfig,
50-
}
41+
if f.experimentDecisionService != nil {
42+
// @TODO: add in a feature decision service that takes into account multiple experiments (via group mutex)
43+
experiment := feature.FeatureExperiments[0]
44+
experimentDecisionContext := ExperimentDecisionContext{
45+
Experiment: &experiment,
46+
ProjectConfig: decisionContext.ProjectConfig,
47+
}
5148

52-
experimentDecision, err := featureService.experimentDecisionService.GetDecision(experimentDecisionContext, userContext)
53-
if err != nil {
54-
// @TODO(mng): handle error here
49+
experimentDecision, err := f.experimentDecisionService.GetDecision(experimentDecisionContext, userContext)
50+
featureDecision := FeatureDecision{
51+
Experiment: experiment,
52+
Decision: experimentDecision.Decision,
53+
Variation: experimentDecision.Variation,
54+
}
55+
return featureDecision, err
5556
}
56-
featureDecision.Experiment = experiment
57-
featureDecision.Decision = experimentDecision.Decision
58-
featureDecision.Variation = experimentDecision.Variation
5957

60-
return featureDecision, nil
58+
featureDecisionContext := FeatureDecisionContext{
59+
Feature: feature,
60+
ProjectConfig: decisionContext.ProjectConfig,
61+
}
62+
featureDecision, err := f.rolloutDecisionService.GetDecision(featureDecisionContext, userContext)
63+
featureDecision.Source = Rollout
64+
return featureDecision, err
6165
}

optimizely/decision/composite_service.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,18 @@ type CompositeService struct {
2828

2929
// NewCompositeService returns a new instance of the DefeaultDecisionEngine
3030
func NewCompositeService() *CompositeService {
31-
experimentDecisionService := NewCompositeExperimentService()
32-
featureDecisionService := NewCompositeFeatureService(experimentDecisionService)
31+
featureDecisionService := NewCompositeFeatureService()
3332
return &CompositeService{
34-
experimentDecisionServices: []ExperimentDecisionService{experimentDecisionService},
35-
featureDecisionServices: []FeatureDecisionService{featureDecisionService},
33+
featureDecisionServices: []FeatureDecisionService{featureDecisionService},
3634
}
3735
}
3836

3937
// GetFeatureDecision returns a decision for the given feature key
40-
func (service CompositeService) GetFeatureDecision(featureDecisionContext FeatureDecisionContext, userContext entities.UserContext) (FeatureDecision, error) {
38+
func (s CompositeService) GetFeatureDecision(featureDecisionContext FeatureDecisionContext, userContext entities.UserContext) (FeatureDecision, error) {
4139
var featureDecision FeatureDecision
4240

4341
// loop through the different features decision services until we get a decision
44-
for _, decisionService := range service.featureDecisionServices {
42+
for _, decisionService := range s.featureDecisionServices {
4543
featureDecision, err := decisionService.GetDecision(featureDecisionContext, userContext)
4644
if err != nil {
4745
// @TODO: log error

optimizely/decision/entities.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ type FeatureDecisionContext struct {
3434
ProjectConfig optimizely.ProjectConfig
3535
}
3636

37+
// Source is where the decision came from
38+
type Source string
39+
40+
const (
41+
// Rollout - the decision came from a rollout
42+
Rollout Source = "Rollout"
43+
)
44+
3745
// Decision contains base information about a decision
3846
type Decision struct {
3947
DecisionMade bool
@@ -43,6 +51,7 @@ type Decision struct {
4351
// FeatureDecision contains the decision information about a feature
4452
type FeatureDecision struct {
4553
Decision
54+
Source Source
4655
Experiment entities.Experiment
4756
Variation entities.Variation
4857
}

0 commit comments

Comments
 (0)