Skip to content

Commit 803c82c

Browse files
authored
Migrate Go SDK to V2 Randomization Endpoint (#16)
* update rule tests * update assignment logic * fix tests * add eppo value * bump version
1 parent 6a5b95f commit 803c82c

12 files changed

+289
-164
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ eppo-golang-sdk-*
1515
# Dependency directories (remove the comment below to include it)
1616
# vendor/
1717
eppoclient/test-data/assignment-v2
18-
eppoclient/test-data/rac-experiments.json
18+
eppoclient/test-data/rac-experiments-v2.json
1919
.vscode

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ testDataDir := eppoclient/test-data/
1717
test-data:
1818
rm -rf $(testDataDir)
1919
mkdir -p $(testDataDir)
20-
gsutil cp gs://sdk-test-data/rac-experiments.json $(testDataDir)
20+
gsutil cp gs://sdk-test-data/rac-experiments-v2.json $(testDataDir)
2121
gsutil cp -r gs://sdk-test-data/assignment-v2 $(testDataDir)
2222

2323
test: test-data

eppoclient/client.go

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,35 +28,47 @@ func newEppoClient(configRequestor iConfigRequestor, assignmentLogger IAssignmen
2828
return ec
2929
}
3030

31-
func (ec *EppoClient) GetAssignment(subjectKey string, experimentKey string, subjectAttributes dictionary) (string, error) {
31+
func (ec *EppoClient) GetAssignment(subjectKey string, flagKey string, subjectAttributes dictionary) (string, error) {
3232
if subjectKey == "" {
3333
panic("no subject key provided")
3434
}
3535

36-
if experimentKey == "" {
37-
panic("no experiment key provided")
36+
if flagKey == "" {
37+
panic("no flag key provided")
3838
}
3939

40-
experimentConfig, err := ec.configRequestor.GetConfiguration(experimentKey)
40+
config, err := ec.configRequestor.GetConfiguration(flagKey)
4141
if err != nil {
4242
return "", err
4343
}
4444

45-
override := getSubjectVariationOverride(experimentConfig, subjectKey)
45+
override := getSubjectVariationOverride(config, subjectKey)
4646

4747
if override != "" {
4848
return override, nil
4949
}
5050

51-
if !experimentConfig.Enabled ||
52-
!subjectAttributesSatisfyRules(subjectAttributes, experimentConfig.Rules) ||
53-
!isInExperimentSample(subjectKey, experimentKey, experimentConfig) {
54-
return "", errors.New("not in sample")
51+
// Check if disabled
52+
if !config.Enabled {
53+
return "", errors.New("the experiment or flag is not enabled")
5554
}
5655

57-
assignmentKey := "assignment-" + subjectKey + "-" + experimentKey
58-
shard := getShard(assignmentKey, int64(experimentConfig.SubjectShards))
59-
variations := experimentConfig.Variations
56+
// Find matching rule
57+
rule, err := findMatchingRule(subjectAttributes, config.Rules)
58+
if err != nil {
59+
return "", err
60+
}
61+
62+
// Check if in sample population
63+
allocation := config.Allocations[rule.AllocationKey]
64+
if !isInExperimentSample(subjectKey, flagKey, config.SubjectShards, allocation.PercentExposure) {
65+
return "", errors.New("subject not part of the sample population")
66+
}
67+
68+
// Get assigned variation
69+
assignmentKey := "assignment-" + subjectKey + "-" + flagKey
70+
shard := getShard(assignmentKey, int64(config.SubjectShards))
71+
variations := allocation.Variations
6072
var variationShard Variation
6173

6274
for _, variation := range variations {
@@ -65,10 +77,10 @@ func (ec *EppoClient) GetAssignment(subjectKey string, experimentKey string, sub
6577
}
6678
}
6779

68-
assignedVariation := variationShard.Name
80+
assignedVariation := variationShard.Value.StringValue()
6981

7082
assignmentEvent := AssignmentEvent{
71-
Experiment: experimentKey,
83+
Experiment: flagKey,
7284
Variation: assignedVariation,
7385
Subject: subjectKey,
7486
Timestamp: time.Now().String(),
@@ -107,17 +119,9 @@ func getSubjectVariationOverride(experimentConfig experimentConfiguration, subje
107119
return ""
108120
}
109121

110-
func subjectAttributesSatisfyRules(subjectAttributes dictionary, rules []rule) bool {
111-
if len(rules) == 0 {
112-
return true
113-
}
114-
115-
return matchesAnyRule(subjectAttributes, rules)
116-
}
117-
118-
func isInExperimentSample(subjectKey string, experimentKey string, experimentConfig experimentConfiguration) bool {
119-
shardKey := "exposure-" + subjectKey + "-" + experimentKey
120-
shard := getShard(shardKey, int64(experimentConfig.SubjectShards))
122+
func isInExperimentSample(subjectKey string, flagKey string, subjectShards int, percentExposure float32) bool {
123+
shardKey := "exposure-" + subjectKey + "-" + flagKey
124+
shard := getShard(shardKey, int64(subjectShards))
121125

122-
return float64(shard) <= float64(experimentConfig.PercentExposure)*float64(experimentConfig.SubjectShards)
126+
return float64(shard) <= float64(percentExposure)*float64(subjectShards)
123127
}

eppoclient/client_test.go

Lines changed: 72 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import (
77
"github.com/stretchr/testify/mock"
88
)
99

10+
var defaultAllocationKey = "allocation-key"
11+
var defaultRule = rule{Conditions: []condition{}, AllocationKey: defaultAllocationKey}
12+
1013
func Test_AssignBlankExperiment(t *testing.T) {
1114
var mockConfigRequestor = new(mockConfigRequestor)
1215
var mockLogger = new(mockLogger)
@@ -28,16 +31,21 @@ func Test_SubjectNotInSample(t *testing.T) {
2831
var mockConfigRequestor = new(mockConfigRequestor)
2932
overrides := make(dictionary)
3033
var mockVariations = []Variation{
31-
{Name: "control", ShardRange: shardRange{Start: 0, End: 10000}},
34+
{Name: "control", Value: String("control"), ShardRange: shardRange{Start: 0, End: 10000}},
3235
}
33-
mockResult := experimentConfiguration{
34-
Name: "recommendation_algo",
36+
var allocations = make(map[string]Allocation)
37+
allocations[defaultAllocationKey] = Allocation{
3538
PercentExposure: 0,
36-
Enabled: true,
37-
SubjectShards: 1000,
38-
Overrides: overrides,
3939
Variations: mockVariations,
4040
}
41+
mockResult := experimentConfiguration{
42+
Name: "recommendation_algo",
43+
Enabled: true,
44+
SubjectShards: 1000,
45+
Overrides: overrides,
46+
Allocations: allocations,
47+
Rules: []rule{defaultRule},
48+
}
4149

4250
mockConfigRequestor.Mock.On("GetConfiguration", mock.Anything).Return(mockResult, nil)
4351

@@ -57,16 +65,21 @@ func Test_LogAssignment(t *testing.T) {
5765
overrides := make(dictionary)
5866

5967
var mockVariations = []Variation{
60-
{Name: "control", ShardRange: shardRange{Start: 0, End: 10000}},
68+
{Name: "control", Value: String("control"), ShardRange: shardRange{Start: 0, End: 10000}},
6169
}
62-
mockResult := experimentConfiguration{
63-
Name: "recommendation_algo",
64-
PercentExposure: 100,
65-
Enabled: true,
66-
SubjectShards: 1000,
67-
Overrides: overrides,
70+
var allocations = make(map[string]Allocation)
71+
allocations[defaultAllocationKey] = Allocation{
72+
PercentExposure: 1,
6873
Variations: mockVariations,
6974
}
75+
mockResult := experimentConfiguration{
76+
Name: "recommendation_algo",
77+
Enabled: true,
78+
SubjectShards: 1000,
79+
Overrides: overrides,
80+
Allocations: allocations,
81+
Rules: []rule{defaultRule},
82+
}
7083
mockConfigRequestor.Mock.On("GetConfiguration", "experiment-key-1").Return(mockResult, nil)
7184

7285
client := newEppoClient(mockConfigRequestor, mockLogger)
@@ -87,16 +100,21 @@ func Test_GetAssignmentHandlesLoggingPanic(t *testing.T) {
87100
overrides := make(dictionary)
88101

89102
var mockVariations = []Variation{
90-
{Name: "control", ShardRange: shardRange{Start: 0, End: 10000}},
103+
{Name: "control", Value: String("control"), ShardRange: shardRange{Start: 0, End: 10000}},
91104
}
92-
mockResult := experimentConfiguration{
93-
Name: "recommendation_algo",
94-
PercentExposure: 100,
95-
Enabled: true,
96-
SubjectShards: 1000,
97-
Overrides: overrides,
105+
var allocations = make(map[string]Allocation)
106+
allocations[defaultAllocationKey] = Allocation{
107+
PercentExposure: 1,
98108
Variations: mockVariations,
99109
}
110+
mockResult := experimentConfiguration{
111+
Name: "recommendation_algo",
112+
Enabled: true,
113+
SubjectShards: 1000,
114+
Overrides: overrides,
115+
Allocations: allocations,
116+
Rules: []rule{defaultRule},
117+
}
100118
mockConfigRequestor.Mock.On("GetConfiguration", "experiment-key-1").Return(mockResult, nil)
101119

102120
client := newEppoClient(mockConfigRequestor, mockLogger)
@@ -113,20 +131,24 @@ func Test_AssignSubjectWithAttributesAndRules(t *testing.T) {
113131
mockLogger.Mock.On("LogAssignment", mock.Anything).Return()
114132

115133
var matchesEmailCondition = condition{Operator: "MATCHES", Value: ".*@eppo.com", Attribute: "email"}
116-
var textRule = rule{Conditions: []condition{matchesEmailCondition}}
134+
var textRule = rule{AllocationKey: defaultAllocationKey, Conditions: []condition{matchesEmailCondition}}
117135
var mockConfigRequestor = new(mockConfigRequestor)
118136
var overrides = make(dictionary)
119137
var mockVariations = []Variation{
120-
{Name: "control", ShardRange: shardRange{Start: 0, End: 10000}},
138+
{Name: "control", Value: String("control"), ShardRange: shardRange{Start: 0, End: 10000}},
121139
}
122-
var mockResult = experimentConfiguration{
123-
Name: "recommendation_algo",
124-
PercentExposure: 100,
125-
Enabled: true,
126-
SubjectShards: 1000,
127-
Overrides: overrides,
140+
var allocations = make(map[string]Allocation)
141+
allocations[defaultAllocationKey] = Allocation{
142+
PercentExposure: 1,
128143
Variations: mockVariations,
129-
Rules: []rule{textRule},
144+
}
145+
var mockResult = experimentConfiguration{
146+
Name: "recommendation_algo",
147+
Enabled: true,
148+
SubjectShards: 1000,
149+
Overrides: overrides,
150+
Rules: []rule{textRule},
151+
Allocations: allocations,
130152
}
131153
mockConfigRequestor.Mock.On("GetConfiguration", "experiment-key-1").Return(mockResult, nil)
132154

@@ -164,14 +186,17 @@ func Test_WithSubjectInOverrides(t *testing.T) {
164186
}
165187
var overrides = make(dictionary)
166188
overrides["d6d7705392bc7af633328bea8c4c6904"] = "override-variation"
167-
var mockResult = experimentConfiguration{
168-
Name: "recommendation_algo",
169-
PercentExposure: 100,
170-
Enabled: true,
171-
SubjectShards: 1000,
172-
Overrides: overrides,
189+
var allocations = make(map[string]Allocation)
190+
allocations[defaultAllocationKey] = Allocation{
191+
PercentExposure: 1,
173192
Variations: mockVariations,
174-
Rules: []rule{textRule},
193+
}
194+
var mockResult = experimentConfiguration{
195+
Name: "recommendation_algo",
196+
Enabled: true,
197+
SubjectShards: 1000,
198+
Overrides: overrides,
199+
Rules: []rule{textRule},
175200
}
176201

177202
mockConfigRequestor.Mock.On("GetConfiguration", "experiment-key-1").Return(mockResult, nil)
@@ -193,14 +218,18 @@ func Test_WithSubjectInOverridesExpDisabled(t *testing.T) {
193218
}
194219
var overrides = make(dictionary)
195220
overrides["d6d7705392bc7af633328bea8c4c6904"] = "override-variation"
196-
var mockResult = experimentConfiguration{
197-
Name: "recommendation_algo",
198-
PercentExposure: 100,
199-
Enabled: false,
200-
SubjectShards: 1000,
201-
Overrides: overrides,
221+
var allocations = make(map[string]Allocation)
222+
allocations[defaultAllocationKey] = Allocation{
223+
PercentExposure: 1,
202224
Variations: mockVariations,
203-
Rules: []rule{textRule},
225+
}
226+
var mockResult = experimentConfiguration{
227+
Name: "recommendation_algo",
228+
Enabled: false,
229+
SubjectShards: 1000,
230+
Overrides: overrides,
231+
Allocations: allocations,
232+
Rules: []rule{textRule},
204233
}
205234

206235
mockConfigRequestor.Mock.On("GetConfiguration", "experiment-key-1").Return(mockResult, nil)

eppoclient/configurationrequestor.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"fmt"
66
)
77

8-
const RAC_ENDPOINT = "/randomized_assignment/config"
8+
const RAC_ENDPOINT = "/randomized_assignment/v2/config"
99

1010
type iConfigRequestor interface {
1111
GetConfiguration(key string) (experimentConfiguration, error)
@@ -48,7 +48,7 @@ func (ecr *experimentConfigurationRequestor) FetchAndStoreConfigurations() {
4848
fmt.Println(err)
4949
}
5050

51-
err = json.Unmarshal(responseBody["experiments"], &configs)
51+
err = json.Unmarshal(responseBody["flags"], &configs)
5252

5353
if err != nil {
5454
fmt.Println("Failed to unmarshal RAC response json in experiments section", result)

eppoclient/configurationstore.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,23 @@ type configurationStore struct {
1313
}
1414

1515
type Variation struct {
16-
Name string `json:"name"`
17-
ShardRange shardRange
16+
Name string `json:"name"`
17+
Value Value `json:"value"`
18+
ShardRange shardRange `json:"shardRange"`
1819
}
1920

20-
type experimentConfiguration struct {
21-
Name string `json:"name"`
21+
type Allocation struct {
2222
PercentExposure float32 `json:"percentExposure"`
23-
Enabled bool `json:"enabled"`
24-
SubjectShards int `json:"subjectShards"`
2523
Variations []Variation `json:"variations"`
26-
Rules []rule `json:"rules"`
27-
Overrides dictionary `json:"overrides"`
24+
}
25+
26+
type experimentConfiguration struct {
27+
Name string `json:"name"`
28+
Enabled bool `json:"enabled"`
29+
SubjectShards int `json:"subjectShards"`
30+
Rules []rule `json:"rules"`
31+
Overrides dictionary `json:"overrides"`
32+
Allocations map[string]Allocation `json:"allocations"`
2833
}
2934

3035
func newConfigurationStore(maxEntries int) *configurationStore {

eppoclient/configurationstore_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import (
77
"github.com/stretchr/testify/assert"
88
)
99

10+
var testAllocationMap = make(map[string]Allocation)
11+
1012
var testExp = experimentConfiguration{
11-
SubjectShards: 1000,
12-
PercentExposure: 1,
13-
Enabled: true,
14-
Variations: []Variation{},
15-
Name: "randomization_algo",
13+
SubjectShards: 1000,
14+
Enabled: true,
15+
Allocations: testAllocationMap,
16+
Rules: []rule{},
17+
Name: "randomization_algo",
1618
}
1719

1820
const TEST_MAX_SIZE = 10

eppoclient/eppoclient_e2e_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
)
1616

1717
const TEST_DATA_DIR = "test-data/assignment-v2"
18-
const MOCK_RAC_RESPONSE_FILE = "test-data/rac-experiments.json"
18+
const MOCK_RAC_RESPONSE_FILE = "test-data/rac-experiments-v2.json"
1919

2020
var tstData = []testData{}
2121

@@ -61,7 +61,7 @@ func initFixture() string {
6161

6262
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
6363
switch strings.TrimSpace(r.URL.Path) {
64-
case "/randomized_assignment/config":
64+
case "/randomized_assignment/v2/config":
6565
json.NewEncoder(w).Encode(testResponse)
6666
default:
6767
http.NotFoundHandler().ServeHTTP(w, r)

eppoclient/initclient.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package eppoclient
44

55
import "net/http"
66

7-
var __version__ = "1.0.0"
7+
var __version__ = "1.1.0"
88

99
// InitClient is required to start polling of experiments configurations and create
1010
// an instance of EppoClient, which could be used to get assignments information.

0 commit comments

Comments
 (0)