Skip to content

Commit e9cfbe4

Browse files
authored
FF-1939 Update to UFC (#41)
* test: use full copy of sdk-test-data Instead of copying just selected files/directories, have the full repository available to tests. * refactor: rename dictionary -> SubjectAttributes * FF-1939 Update to UFC * FF-1581 Make polling interval configurable * FF-1581 refactor: make PollerInterval a time.Duration * FF-1939 Bump major version to v3 * FF-1939 Handle unlikely case of failed cast * FF-1939 Panic less * FF-1939 Fix tests to use proper shard ranges * FF-1939 Make ONE_OF case-sensitive * FF-1939 Add a comment for the defaulting code * FF-1939 Make flagConfiguration.TotalShards non-optional * FF-1939 Better handling for ONE_OF * FF-1939 Support bool for MATCHES, add NOT_MATCHES
1 parent 064d11b commit e9cfbe4

24 files changed

+870
-988
lines changed

Makefile

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,12 @@ help: Makefile
1414

1515
## test-data
1616
testDataDir := eppoclient/test-data/
17-
tempDir := ${testDataDir}temp/
18-
gitDataDir := ${tempDir}sdk-test-data/
1917
branchName := main
2018
githubRepoLink := https://github.com/Eppo-exp/sdk-test-data.git
2119
.PHONY: test-data
2220
test-data:
2321
rm -rf $(testDataDir)
24-
mkdir -p $(tempDir)
25-
git clone -b ${branchName} --depth 1 --single-branch ${githubRepoLink} ${gitDataDir}
26-
cp ${gitDataDir}rac-experiments-v3.json ${testDataDir}
27-
cp -r ${gitDataDir}assignment-v2 ${testDataDir}
28-
rm -rf ${tempDir}
22+
git clone -b ${branchName} --depth 1 --single-branch ${githubRepoLink} ${testDataDir}
2923

3024
test: test-data
3125
go test ./...

eppoclient/assignmentlogger.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ type IAssignmentLogger interface {
77
}
88

99
type AssignmentEvent struct {
10-
Experiment string `json:"experiment"`
11-
FeatureFlag string `json:"featureFlag"`
12-
Allocation string `json:"allocation"`
13-
Variation Value `json:"variation"`
14-
Subject string `json:"subject"`
15-
Timestamp string `json:"timestamp"`
16-
SubjectAttributes dictionary `json:"subjectAttributes,omitempty"`
10+
Experiment string `json:"experiment"`
11+
FeatureFlag string `json:"featureFlag"`
12+
Allocation string `json:"allocation"`
13+
Variation string `json:"variation"`
14+
Subject string `json:"subject"`
15+
SubjectAttributes SubjectAttributes `json:"subjectAttributes,omitempty"`
16+
Timestamp string `json:"timestamp"`
17+
MetaData map[string]string `json:"metaData"`
18+
ExtraLogging map[string]string `json:"extraLogging,omitempty"`
1719
}
1820

1921
type AssignmentLogger struct {

eppoclient/assignmentlogger_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,45 +15,45 @@ func TestAssignmentEventSerialization(t *testing.T) {
1515
Experiment: "testExperiment",
1616
FeatureFlag: "testFeatureFlag",
1717
Allocation: "testAllocation",
18-
Variation: String("testVariation"),
18+
Variation: "testVariation",
1919
Subject: "testSubject",
2020
Timestamp: "testTimestamp",
2121
},
2222
{
2323
Experiment: "testExperiment",
2424
FeatureFlag: "testFeatureFlag",
2525
Allocation: "testAllocation",
26-
Variation: Bool(true),
26+
Variation: "true",
2727
Subject: "testSubject",
2828
Timestamp: "testTimestamp",
29-
SubjectAttributes: dictionary{"testKey": "testValue"},
29+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
3030
},
3131
{
3232
Experiment: "testExperiment",
3333
FeatureFlag: "testFeatureFlag",
3434
Allocation: "testAllocation",
35-
Variation: Numeric(123.45),
35+
Variation: "123.45",
3636
Subject: "testSubject",
3737
Timestamp: "testTimestamp",
38-
SubjectAttributes: dictionary{"testKey": "testValue"},
38+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
3939
},
4040
{
4141
Experiment: "testExperiment",
4242
FeatureFlag: "testFeatureFlag",
4343
Allocation: "testAllocation",
44-
Variation: String("testVariation"),
44+
Variation: "testVariation",
4545
Subject: "testSubject",
4646
Timestamp: "testTimestamp",
47-
SubjectAttributes: dictionary{"testKey": "testValue"},
47+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
4848
},
4949
{
5050
Experiment: "testExperiment",
5151
FeatureFlag: "testFeatureFlag",
5252
Allocation: "testAllocation",
53-
Variation: String("{\"foo\":\"bar\",\"car\":\"far\"}"),
53+
Variation: "jsonVariation",
5454
Subject: "testSubject",
5555
Timestamp: "testTimestamp",
56-
SubjectAttributes: dictionary{"testKey": "testValue"},
56+
SubjectAttributes: SubjectAttributes{"testKey": "testValue"},
5757
},
5858
}
5959

eppoclient/client.go

Lines changed: 70 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
package eppoclient
22

33
import (
4-
"crypto/md5"
5-
"encoding/hex"
6-
"errors"
74
"fmt"
85
)
96

7+
type SubjectAttributes map[string]interface{}
8+
109
// Client for eppo.cloud. Instance of this struct will be created on calling InitClient.
1110
// EppoClient will then immediately start polling experiments data from Eppo.
1211
type EppoClient struct {
@@ -15,43 +14,73 @@ type EppoClient struct {
1514
logger IAssignmentLogger
1615
}
1716

18-
func newEppoClient(configRequestor iConfigRequestor, assignmentLogger IAssignmentLogger) *EppoClient {
17+
func newEppoClient(configRequestor iConfigRequestor, poller *poller, assignmentLogger IAssignmentLogger) *EppoClient {
1918
var ec = &EppoClient{}
2019

21-
var poller = newPoller(10, configRequestor.FetchAndStoreConfigurations)
2220
ec.poller = *poller
2321
ec.configRequestor = configRequestor
2422
ec.logger = assignmentLogger
2523

2624
return ec
2725
}
2826

29-
// GetAssignment is maintained for backwards capability. It will return a string value for the assignment.
30-
func (ec *EppoClient) GetAssignment(subjectKey string, flagKey string, subjectAttributes dictionary) (string, error) {
31-
return ec.GetStringAssignment(subjectKey, flagKey, subjectAttributes)
27+
func (ec *EppoClient) GetBoolAssignment(subjectKey string, flagKey string, subjectAttributes SubjectAttributes, defaultValue bool) (bool, error) {
28+
variation, err := ec.getAssignment(subjectKey, flagKey, subjectAttributes, booleanVariation)
29+
if err != nil || variation == nil {
30+
return defaultValue, err
31+
}
32+
result, ok := variation.(bool)
33+
if !ok {
34+
return defaultValue, fmt.Errorf("failed to cast %v to bool", variation)
35+
}
36+
return result, err
3237
}
3338

34-
func (ec *EppoClient) GetBoolAssignment(subjectKey string, flagKey string, subjectAttributes dictionary) (bool, error) {
35-
variation, err := ec.getAssignment(subjectKey, flagKey, subjectAttributes, BoolType)
36-
return variation.BoolValue, err
39+
func (ec *EppoClient) GetNumericAssignment(subjectKey string, flagKey string, subjectAttributes SubjectAttributes, defaultValue float64) (float64, error) {
40+
variation, err := ec.getAssignment(subjectKey, flagKey, subjectAttributes, numericVariation)
41+
if err != nil || variation == nil {
42+
return defaultValue, err
43+
}
44+
result, ok := variation.(float64)
45+
if !ok {
46+
return defaultValue, fmt.Errorf("failed to cast %v to float64", variation)
47+
}
48+
return result, err
3749
}
3850

39-
func (ec *EppoClient) GetNumericAssignment(subjectKey string, flagKey string, subjectAttributes dictionary) (float64, error) {
40-
variation, err := ec.getAssignment(subjectKey, flagKey, subjectAttributes, NumericType)
41-
return variation.NumericValue, err
51+
func (ec *EppoClient) GetIntegerAssignment(subjectKey string, flagKey string, subjectAttributes SubjectAttributes, defaultValue int64) (int64, error) {
52+
variation, err := ec.getAssignment(subjectKey, flagKey, subjectAttributes, integerVariation)
53+
if err != nil || variation == nil {
54+
return defaultValue, err
55+
}
56+
result, ok := variation.(int64)
57+
if !ok {
58+
return defaultValue, fmt.Errorf("failed to cast %v to int64", variation)
59+
}
60+
return result, err
4261
}
4362

44-
func (ec *EppoClient) GetStringAssignment(subjectKey string, flagKey string, subjectAttributes dictionary) (string, error) {
45-
variation, err := ec.getAssignment(subjectKey, flagKey, subjectAttributes, StringType)
46-
return variation.StringValue, err
63+
func (ec *EppoClient) GetStringAssignment(subjectKey string, flagKey string, subjectAttributes SubjectAttributes, defaultValue string) (string, error) {
64+
variation, err := ec.getAssignment(subjectKey, flagKey, subjectAttributes, stringVariation)
65+
if err != nil || variation == nil {
66+
return defaultValue, err
67+
}
68+
result, ok := variation.(string)
69+
if !ok {
70+
return defaultValue, fmt.Errorf("failed to cast %v to string", variation)
71+
}
72+
return result, err
4773
}
4874

49-
func (ec *EppoClient) GetJSONStringAssignment(subjectKey string, flagKey string, subjectAttributes dictionary) (string, error) {
50-
variation, err := ec.getAssignment(subjectKey, flagKey, subjectAttributes, StringType)
51-
return variation.StringValue, err
75+
func (ec *EppoClient) GetJSONAssignment(subjectKey string, flagKey string, subjectAttributes SubjectAttributes, defaultValue interface{}) (interface{}, error) {
76+
variation, err := ec.getAssignment(subjectKey, flagKey, subjectAttributes, jsonVariation)
77+
if err != nil || variation == nil {
78+
return defaultValue, err
79+
}
80+
return variation, err
5281
}
5382

54-
func (ec *EppoClient) getAssignment(subjectKey string, flagKey string, subjectAttributes dictionary, valueType ValueType) (Value, error) {
83+
func (ec *EppoClient) getAssignment(subjectKey string, flagKey string, subjectAttributes SubjectAttributes, variationType variationType) (interface{}, error) {
5584
if subjectKey == "" {
5685
panic("no subject key provided")
5786
}
@@ -60,86 +89,35 @@ func (ec *EppoClient) getAssignment(subjectKey string, flagKey string, subjectAt
6089
panic("no flag key provided")
6190
}
6291

63-
config, err := ec.configRequestor.GetConfiguration(flagKey)
92+
flag, err := ec.configRequestor.GetConfiguration(flagKey)
6493
if err != nil {
65-
return Null(), err
66-
}
67-
68-
override := getSubjectVariationOverride(config, subjectKey, valueType)
69-
if override != Null() {
70-
return override, nil
71-
}
72-
73-
// Check if disabled
74-
if !config.Enabled {
75-
return Null(), errors.New("the experiment or flag is not enabled")
94+
return nil, err
7695
}
7796

78-
// Find matching rule
79-
rule, err := findMatchingRule(subjectAttributes, config.Rules)
97+
err = flag.verifyType(variationType)
8098
if err != nil {
81-
return Null(), err
82-
}
83-
84-
// Check if in sample population
85-
allocation := config.Allocations[rule.AllocationKey]
86-
if !isInExperimentSample(subjectKey, flagKey, config.SubjectShards, allocation.PercentExposure) {
87-
return Null(), errors.New("subject not part of the sample population")
99+
return nil, err
88100
}
89101

90-
// Get assigned variation
91-
assignmentKey := "assignment-" + subjectKey + "-" + flagKey
92-
shard := getShard(assignmentKey, config.SubjectShards)
93-
variations := allocation.Variations
94-
var variationShard Variation
95-
96-
for _, variation := range variations {
97-
if isShardInRange(shard, variation.ShardRange) {
98-
variationShard = variation
99-
}
102+
assignmentValue, assignmentEvent, err := flag.eval(subjectKey, subjectAttributes)
103+
if err != nil {
104+
return nil, err
100105
}
101106

102-
assignedVariation := variationShard.Value
103-
104-
func() {
105-
// need to catch panics from Logger and continue
106-
defer func() {
107-
r := recover()
108-
if r != nil {
109-
fmt.Println("panic occurred:", r)
110-
}
107+
if assignmentEvent != nil {
108+
func() {
109+
// need to catch panics from Logger and continue
110+
defer func() {
111+
r := recover()
112+
if r != nil {
113+
fmt.Println("panic occurred:", r)
114+
}
115+
}()
116+
117+
// Log assignment
118+
ec.logger.LogAssignment(*assignmentEvent)
111119
}()
112-
113-
// Log assignment
114-
assignmentEvent := AssignmentEvent{
115-
Experiment: flagKey + "-" + rule.AllocationKey,
116-
FeatureFlag: flagKey,
117-
Allocation: rule.AllocationKey,
118-
Variation: assignedVariation,
119-
Subject: subjectKey,
120-
Timestamp: TimeNow(),
121-
SubjectAttributes: subjectAttributes,
122-
}
123-
ec.logger.LogAssignment(assignmentEvent)
124-
}()
125-
126-
return assignedVariation, nil
127-
}
128-
129-
func getSubjectVariationOverride(experimentConfig experimentConfiguration, subject string, valueType ValueType) Value {
130-
hash := md5.Sum([]byte(subject))
131-
hashOutput := hex.EncodeToString(hash[:])
132-
133-
if val, ok := experimentConfig.Overrides[hashOutput]; ok {
134-
return val
135120
}
136121

137-
return Null()
138-
}
139-
140-
func isInExperimentSample(subjectKey string, flagKey string, subjectShards int64, percentExposure float32) bool {
141-
shardKey := "exposure-" + subjectKey + "-" + flagKey
142-
shard := getShard(shardKey, subjectShards)
143-
144-
return float64(shard) <= float64(percentExposure)*float64(subjectShards)
122+
return assignmentValue, nil
145123
}

0 commit comments

Comments
 (0)