Skip to content

Commit c838b82

Browse files
author
Dave Johnston
committed
[FFM-1219]: Fix how segment rules are processed
What: * change logic so the include/exclude rules use target identifier rather name to be consistent with how the rules are stored. * Make exclude list take precedence over include list and rules. * Process segment rules as OR rather than AND. Why: When creating segment include/exclude lists the server only stores target identifiers not names, so this logic needed to be updated so the lists were correctly evaluated. The exclude list was being evaluated last, which meant we could not include all users via a rule, but exclude one via a list. This is consistent with how the Java SDK behaves. Segment rules should be OR'd, but they were being treated like flag clauses and were being AND'd which meant conflicting rules would prevent targets from correctly being included.
1 parent 873a19f commit c838b82

File tree

6 files changed

+139
-68
lines changed

6 files changed

+139
-68
lines changed

evaluation/feature.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"reflect"
77
"strconv"
88

9-
"github.com/labstack/gommon/log"
9+
"github.com/drone/ff-golang-server-sdk/log"
1010

1111
"github.com/drone/ff-golang-server-sdk/types"
1212
)
@@ -88,7 +88,7 @@ func (c Clauses) Evaluate(target *Target, segments Segments) bool {
8888
// operator should be evaluated based on type of attribute
8989
op, err := target.GetOperator(clause.Attribute)
9090
if err != nil {
91-
fmt.Print(err)
91+
log.Warn(err)
9292
}
9393
if !clause.Evaluate(target, segments, op) {
9494
return false

evaluation/segment.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package evaluation
22

3-
import "strings"
3+
import (
4+
"strings"
5+
6+
"github.com/drone/ff-golang-server-sdk/log"
7+
)
48

59
// StrSlice helper type used for string slice operations
610
type StrSlice []string
@@ -25,6 +29,32 @@ func (slice StrSlice) ContainsSensitive(s string) bool {
2529
return false
2630
}
2731

32+
// SegmentRules is a set of clauses to determine if a target should be included in the segment.
33+
type SegmentRules Clauses
34+
35+
// Evaluate SegmentRules. This determines if a segment rule is being used to include a target with
36+
// the segment. SegmentRules are similar to ServingRules except a ServingRule can contain multiple clauses
37+
// but a Segment rule only contains one clause.
38+
//
39+
func (c SegmentRules) Evaluate(target *Target, segments Segments) bool {
40+
// OR operation
41+
for _, clause := range c {
42+
// operator should be evaluated based on type of attribute
43+
op, err := target.GetOperator(clause.Attribute)
44+
if err != nil {
45+
log.Warn(err)
46+
continue
47+
}
48+
if clause.Evaluate(target, segments, op) {
49+
return true
50+
}
51+
52+
// continue on next rule
53+
}
54+
// it means that there was no matching rule
55+
return false
56+
}
57+
2858
// Segment object used in feature flag evaluation.
2959
// Examples: beta users, premium customers
3060
type Segment struct {
@@ -41,24 +71,33 @@ type Segment struct {
4171
Included StrSlice
4272

4373
// An array of rules that can cause a user to be included in this segment.
44-
Rules Clauses
74+
Rules SegmentRules
4575
Tags []Tag
4676
Version int64
4777
}
4878

4979
// Evaluate segment based on target input
5080
func (s Segment) Evaluate(target *Target) bool {
81+
82+
// is target excluded from segment via the exclude list
83+
if s.Excluded.ContainsSensitive(target.Identifier) {
84+
log.Debugf("target %s excluded from segment %s via exclude list\n", target.Identifier, s.Identifier)
85+
return false
86+
}
87+
88+
// is target included from segment via the include list
5189
if s.Included.ContainsSensitive(target.Identifier) {
90+
log.Debugf("target %s included in segment %s via include list\n", target.Identifier, s.Identifier)
5291
return true
5392
}
5493

94+
// is target included in the segment via the clauses
5595
if s.Rules.Evaluate(target, nil) {
96+
log.Debugf("target %s included in segment %s via rules\n", target.Identifier, s.Identifier)
5697
return true
5798
}
5899

59-
if s.Excluded.ContainsSensitive(target.Identifier) {
60-
return true
61-
}
100+
log.Debugf("No rules to include target %s in segment %s\n", target.Identifier, s.Identifier)
62101
return false
63102
}
64103

evaluation/segment_test.go

Lines changed: 19 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,17 @@
11
package evaluation
22

33
import (
4-
"github.com/google/uuid"
5-
64
"testing"
75
)
86

97
func TestSegment_Evaluate(t *testing.T) {
108
type fields struct {
11-
Identifier string
12-
Name string
13-
CreatedAt *int64
14-
ModifiedAt *int64
15-
Environment *string
16-
Excluded StrSlice
17-
Included StrSlice
18-
Rules Clauses
19-
Tags []Tag
20-
Version int64
21-
}
22-
type args struct {
23-
target *Target
9+
Identifier string
10+
Excluded StrSlice
11+
Included StrSlice
12+
Rules SegmentRules
2413
}
14+
2515
f := false
2616
m := make(map[string]interface{})
2717
m["email"] = "[email protected]"
@@ -35,54 +25,28 @@ func TestSegment_Evaluate(t *testing.T) {
3525
tests := []struct {
3626
name string
3727
fields fields
38-
args args
28+
args Target
3929
want bool
4030
}{
41-
{name: "test included", fields: struct {
42-
Identifier string
43-
Name string
44-
CreatedAt *int64
45-
ModifiedAt *int64
46-
Environment *string
47-
Excluded StrSlice
48-
Included StrSlice
49-
Rules Clauses
50-
Tags []Tag
51-
Version int64
52-
}{Identifier: "beta", Name: "Beta users", CreatedAt: nil, ModifiedAt: nil, Environment: nil, Excluded: nil,
53-
Included: []string{"john"}, Rules: nil, Tags: nil, Version: 1}, args: struct{ target *Target }{target: &target}, want: true},
54-
{name: "test rules", fields: struct {
55-
Identifier string
56-
Name string
57-
CreatedAt *int64
58-
ModifiedAt *int64
59-
Environment *string
60-
Excluded StrSlice
61-
Included StrSlice
62-
Rules Clauses
63-
Tags []Tag
64-
Version int64
65-
}{Identifier: "beta", Name: "Beta users", CreatedAt: nil, ModifiedAt: nil, Environment: nil, Excluded: nil,
66-
Included: nil, Rules: []Clause{
67-
{Attribute: "email", ID: uuid.New().String(), Negate: false, Op: equalOperator, Value: []string{"[email protected]"}},
68-
}, Tags: nil, Version: 1}, args: struct{ target *Target }{target: &target}, want: true},
31+
{name: "test target included by list", fields: fields{Identifier: "beta", Included: []string{"john"}}, args: target, want: true},
32+
{name: "test target excluded by list", fields: fields{Identifier: "beta", Included: []string{"john"}, Excluded: []string{"john"}}, args: target, want: false},
33+
{name: "test target included by rules", fields: fields{Identifier: "beta", Rules: []Clause{{Attribute: "email", ID: "1", Op: equalOperator, Value: []string{"[email protected]"}}}}, args: target, want: true},
34+
{name: "test target not included by rules", fields: fields{Identifier: "beta", Rules: []Clause{{Attribute: "email", ID: "2", Op: equalOperator, Value: []string{"[email protected]"}}}}, args: target, want: false},
35+
{name: "test target rules evaluating with OR", fields: fields{Identifier: "beta", Rules: []Clause{
36+
{Attribute: "email", ID: "1", Op: equalOperator, Value: []string{"[email protected]"}},
37+
{Attribute: "email", ID: "2", Op: equalOperator, Value: []string{"[email protected]"}},
38+
}}, args: target, want: true},
6939
}
7040
for _, tt := range tests {
7141
val := tt
7242
t.Run(val.name, func(t *testing.T) {
7343
s := Segment{
74-
Identifier: val.fields.Identifier,
75-
Name: val.fields.Name,
76-
CreatedAt: val.fields.CreatedAt,
77-
ModifiedAt: val.fields.ModifiedAt,
78-
Environment: val.fields.Environment,
79-
Excluded: val.fields.Excluded,
80-
Included: val.fields.Included,
81-
Rules: val.fields.Rules,
82-
Tags: val.fields.Tags,
83-
Version: val.fields.Version,
44+
Identifier: val.fields.Identifier,
45+
Excluded: val.fields.Excluded,
46+
Included: val.fields.Included,
47+
Rules: val.fields.Rules,
8448
}
85-
if got := s.Evaluate(val.args.target); got != val.want {
49+
if got := s.Evaluate(&val.args); got != val.want {
8650
t.Errorf("Evaluate() = %v, want %v", got, val.want)
8751
}
8852
})

evaluation/target.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,6 @@ func (t Target) GetOperator(attr string) (types.ValueType, error) {
4646
reflect.Invalid, reflect.Map, reflect.Ptr, reflect.Slice, reflect.Struct, reflect.Uintptr, reflect.UnsafePointer:
4747
fallthrough
4848
default:
49-
return nil, fmt.Errorf("unexpected type: %s", value.Kind().String())
49+
return nil, fmt.Errorf("unexpected type: %s\n", value.Kind().String())
5050
}
5151
}

log/log.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package log
2+
3+
import "github.com/drone/ff-golang-server-sdk/logger"
4+
5+
// And just go global.
6+
var defaultLogger logger.Logger
7+
8+
// init creates the default logger. This can be changed
9+
func init() {
10+
defaultLogger, _ = logger.NewZapLogger(false)
11+
}
12+
13+
// SetLogger sets the default logger to be used by this package
14+
func SetLogger(logger logger.Logger) {
15+
defaultLogger = logger
16+
}
17+
18+
// Error logs an error message with the parameters
19+
func Error(args ...interface{}) {
20+
defaultLogger.Error(args...)
21+
}
22+
23+
// Errorf logs a formatted error message
24+
func Errorf(format string, args ...interface{}) {
25+
defaultLogger.Errorf(format, args...)
26+
}
27+
28+
// Fatalf logs a formatted fatal message. This will terminate the application
29+
func Fatalf(format string, args ...interface{}) {
30+
defaultLogger.Fatalf(format, args...)
31+
}
32+
33+
// Fatal logs an fatal message with the parameters. This will terminate the application
34+
func Fatal(args ...interface{}) {
35+
defaultLogger.Fatal(args...)
36+
}
37+
38+
// Infof logs a formatted info message
39+
func Infof(format string, args ...interface{}) {
40+
defaultLogger.Infof(format, args...)
41+
}
42+
43+
// Info logs an info message with the parameters
44+
func Info(args ...interface{}) {
45+
defaultLogger.Info(args...)
46+
}
47+
48+
// Warn logs an warn message with the parameters
49+
func Warn(args ...interface{}) {
50+
defaultLogger.Warn(args...)
51+
}
52+
53+
// Warnf logs a formatted warn message
54+
func Warnf(format string, args ...interface{}) {
55+
defaultLogger.Warnf(format, args...)
56+
}
57+
58+
// Debugf logs a formatted debug message
59+
func Debugf(format string, args ...interface{}) {
60+
defaultLogger.Debugf(format, args...)
61+
}
62+
63+
// Debug logs an debug message with the parameters
64+
func Debug(args ...interface{}) {
65+
defaultLogger.Debug(args...)
66+
}

rest/adapter.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package rest
22

3-
import "github.com/drone/ff-golang-server-sdk/evaluation"
3+
import (
4+
"github.com/drone/ff-golang-server-sdk/evaluation"
5+
)
46

57
func (wv WeightedVariation) convert() *evaluation.WeightedVariation {
68
return &evaluation.WeightedVariation{
@@ -144,21 +146,21 @@ func (s Segment) Convert() evaluation.Segment {
144146
if s.Excluded != nil {
145147
excluded = make(evaluation.StrSlice, len(*s.Excluded))
146148
for i, excl := range *s.Excluded {
147-
excluded[i] = excl.Name
149+
excluded[i] = excl.Identifier
148150
}
149151
}
150152

151153
included := make(evaluation.StrSlice, 0)
152154
if s.Included != nil {
153155
included = make(evaluation.StrSlice, len(*s.Included))
154156
for i, incl := range *s.Included {
155-
included[i] = incl.Name
157+
included[i] = incl.Identifier
156158
}
157159
}
158160

159-
rules := make(evaluation.Clauses, 0)
161+
rules := make(evaluation.SegmentRules, 0)
160162
if s.Rules != nil {
161-
rules = make(evaluation.Clauses, len(*s.Rules))
163+
rules = make(evaluation.SegmentRules, len(*s.Rules))
162164
for i, rule := range *s.Rules {
163165
rules[i] = evaluation.Clause{
164166
Attribute: rule.Attribute,

0 commit comments

Comments
 (0)