Skip to content

Commit f62bc72

Browse files
colebaileygitbeeme1mrKavindu-Dodan
authored
feat!: allow custom seed when using targetingKey override for fractional op (#1266)
<!-- Please use this template for your pull request. --> <!-- Please use the sections that you need and delete other sections --> ## This PR <!-- add the description of the PR here --> - no longer injects flagKey as seed for fractional op when user has provided custom targeting - updates schema to allow `cat` and other operations so that custom targeting can be properly seeded ### Related Issues <!-- add here the GitHub issue that this PR resolves if applicable --> #1264 ### Notes <!-- any additional notes for this PR --> ### Follow-up Tasks <!-- anything that is related to this PR but not done here should be noted under this section --> <!-- if there is a need for a new issue, please link it here --> ### How to test <!-- if applicable, add testing instructions under this section --> ```bash # unit tests make test # gherkin tests ./bin/flagd start -f file:test-harness/flags/testing-flags.json -f file:test-harness/flags/custom-ops.json -f file:test-harness/flags/evaluator-refs.json -f file:test-harness/flags/zero-flags.json make flagd-integration-test ``` --------- Signed-off-by: Cole Bailey <[email protected]> Signed-off-by: Cole Bailey <[email protected]> Signed-off-by: Kavindu Dodanduwa <[email protected]> Co-authored-by: Michael Beemer <[email protected]> Co-authored-by: Kavindu Dodanduwa <[email protected]>
1 parent 0075932 commit f62bc72

File tree

18 files changed

+224
-33
lines changed

18 files changed

+224
-33
lines changed

.github/workflows/build.yaml

+2-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ jobs:
137137
-f file:${{ github.workspace }}/test-harness/flags/testing-flags.json \
138138
-f file:${{ github.workspace }}/test-harness/flags/custom-ops.json \
139139
-f file:${{ github.workspace }}/test-harness/flags/evaluator-refs.json \
140-
-f file:${{ github.workspace }}/test-harness/flags/zero-flags.json &
140+
-f file:${{ github.workspace }}/test-harness/flags/zero-flags.json \
141+
-f file:${{ github.workspace }}/test-harness/flags/edge-case-flags.json &
141142
142143
- name: Run evaluation test suite
143144
run: go clean -testcache && go test -cover ./test/integration

core/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ require (
1111
connectrpc.com/otelconnect v0.7.0
1212
github.com/diegoholiveira/jsonlogic/v3 v3.4.0
1313
github.com/fsnotify/fsnotify v1.7.0
14-
github.com/open-feature/flagd-schemas v0.2.9-0.20240215170351-8c72c14eebff
14+
github.com/open-feature/flagd-schemas v0.2.9-0.20240408192555-ea4f119d2bd7
1515
github.com/open-feature/open-feature-operator/apis v0.2.39
1616
github.com/prometheus/client_golang v1.19.0
1717
github.com/robfig/cron v1.2.0

core/go.sum

+4-2
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,7 @@ github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWH
435435
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
436436
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
437437
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
438+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
438439
github.com/diegoholiveira/jsonlogic/v3 v3.4.0 h1:TN++nRmEMA5UHzKl8MJ1kbF5SSzWtKHE0PZ6ITbJeH4=
439440
github.com/diegoholiveira/jsonlogic/v3 v3.4.0/go.mod h1:9oE8z9G+0OMxOoLHF3fhek3KuqD5CBqM0B6XFL08MSg=
440441
github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk=
@@ -626,8 +627,8 @@ github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY
626627
github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw=
627628
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
628629
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
629-
github.com/open-feature/flagd-schemas v0.2.9-0.20240215170351-8c72c14eebff h1:ZJwqlDjz+vfMzs+pYXqThCg3YV8wXxON8YztweiuaQ0=
630-
github.com/open-feature/flagd-schemas v0.2.9-0.20240215170351-8c72c14eebff/go.mod h1:WKtwo1eW9/K6D+4HfgTXWBqCDzpvMhDa5eRxW7R5B2U=
630+
github.com/open-feature/flagd-schemas v0.2.9-0.20240408192555-ea4f119d2bd7 h1:oP+BH8RiNEmSWTffKEXz2ciwen7wbvyX0fESx0aoJ80=
631+
github.com/open-feature/flagd-schemas v0.2.9-0.20240408192555-ea4f119d2bd7/go.mod h1:WKtwo1eW9/K6D+4HfgTXWBqCDzpvMhDa5eRxW7R5B2U=
631632
github.com/open-feature/open-feature-operator/apis v0.2.39 h1:uzZplxAes8/KQWVpC3AE0ME2CNNks2gaaufSZUBkVnM=
632633
github.com/open-feature/open-feature-operator/apis v0.2.39/go.mod h1:I/4tLd5D4JpWpaFZxe2o8R2S1isWGNwHDSC/H5h7o3A=
633634
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -637,6 +638,7 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ
637638
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
638639
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
639640
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
641+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
640642
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
641643
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
642644
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=

core/pkg/evaluator/fractional.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,20 @@ func parseFractionalEvaluationData(values, data any) (string, []fractionalEvalua
5656
if ok {
5757
valuesArray = valuesArray[1:]
5858
} else {
59-
bucketBy, ok = dataMap[targetingKeyKey].(string)
59+
targetingKey, ok := dataMap[targetingKeyKey].(string)
6060
if !ok {
6161
return "", nil, errors.New("bucketing value not supplied and no targetingKey in context")
6262
}
63+
64+
bucketBy = fmt.Sprintf("%s%s", properties.FlagKey, targetingKey)
6365
}
6466

6567
feDistributions, err := parseFractionalEvaluationDistributions(valuesArray)
6668
if err != nil {
6769
return "", nil, err
6870
}
6971

70-
return fmt.Sprintf("%s%s", properties.FlagKey, bucketBy), feDistributions, nil
72+
return bucketBy, feDistributions, nil
7173
}
7274

7375
func parseFractionalEvaluationDistributions(values []any) ([]fractionalEvaluationDistribution, error) {

core/pkg/evaluator/fractional_test.go

+69-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func TestFractionalEvaluation(t *testing.T) {
3232
},
3333
{
3434
"fractional": [
35-
{"var": "email"},
35+
{"cat": [{"var": "$flagd.flagKey"}, {"var": "email"}]},
3636
[
3737
"red",
3838
25
@@ -54,6 +54,34 @@ func TestFractionalEvaluation(t *testing.T) {
5454
]
5555
}`),
5656
},
57+
"customSeededHeaderColor": {
58+
State: "ENABLED",
59+
DefaultVariant: "red",
60+
Variants: map[string]any{
61+
"red": "#FF0000",
62+
"blue": "#0000FF",
63+
"green": "#00FF00",
64+
"yellow": "#FFFF00",
65+
},
66+
Targeting: []byte(`{
67+
"if": [
68+
{
69+
"in": ["@faas.com", {
70+
"var": ["email"]
71+
}]
72+
},
73+
{
74+
"fractional": [
75+
{"cat": ["my-seed", {"var": "email"}]},
76+
["red",25],
77+
["blue",25],
78+
["green",25],
79+
["yellow",25]
80+
]
81+
}, null
82+
]
83+
}`),
84+
},
5785
},
5886
}
5987

@@ -106,6 +134,46 @@ func TestFractionalEvaluation(t *testing.T) {
106134
expectedValue: "#00FF00",
107135
expectedReason: model.TargetingMatchReason,
108136
},
137+
"[email protected] with custom seed": {
138+
flags: flags,
139+
flagKey: "customSeededHeaderColor",
140+
context: map[string]any{
141+
"email": "[email protected]",
142+
},
143+
expectedVariant: "green",
144+
expectedValue: "#00FF00",
145+
expectedReason: model.TargetingMatchReason,
146+
},
147+
"[email protected] with custom seed": {
148+
flags: flags,
149+
flagKey: "customSeededHeaderColor",
150+
context: map[string]any{
151+
"email": "[email protected]",
152+
},
153+
expectedVariant: "red",
154+
expectedValue: "#FF0000",
155+
expectedReason: model.TargetingMatchReason,
156+
},
157+
"[email protected] with custom seed": {
158+
flags: flags,
159+
flagKey: "customSeededHeaderColor",
160+
context: map[string]any{
161+
"email": "[email protected]",
162+
},
163+
expectedVariant: "green",
164+
expectedValue: "#00FF00",
165+
expectedReason: model.TargetingMatchReason,
166+
},
167+
"[email protected] with custom seed": {
168+
flags: flags,
169+
flagKey: "customSeededHeaderColor",
170+
context: map[string]any{
171+
"email": "[email protected]",
172+
},
173+
expectedVariant: "green",
174+
expectedValue: "#00FF00",
175+
expectedReason: model.TargetingMatchReason,
176+
},
109177
"[email protected] with different flag key": {
110178
flags: Flags{
111179
Flags: map[string]model.Flag{

docs/reference/custom-operations/fractional-operation.md

+17-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ OpenFeature allows clients to pass contextual information which can then be used
1010
// Factional evaluation property name used in a targeting rule
1111
"fractional": [
1212
// Evaluation context property used to determine the split
13-
{ "var": "email" },
13+
// Note using `cat` and `$flagd.flagKey` is the suggested default to seed your hash value and prevent bucketing collisions
14+
{
15+
"cat": [
16+
{ "var": "$flagd.flagKey" },
17+
{ "var": "email" }
18+
]
19+
},
1420
// Split definitions contain an array with a variant and percentage
1521
// Percentages must add up to 100
1622
[
@@ -33,7 +39,7 @@ The `defaultVariant` is `red`, but it contains a [targeting rule](../flag-defini
3339

3440
In this case, `25%` of the evaluations will receive `red`, `25%` will receive `blue`, and so on.
3541

36-
Assignment is deterministic (sticky) based on the expression supplied as the first parameter (`{ "var": "email" }`, in this case).
42+
Assignment is deterministic (sticky) based on the expression supplied as the first parameter (`{ "cat": [{ "var": "$flagd.flagKey" }, { "var": "email" }]}`, in this case).
3743
The value retrieved by this expression is referred to as the "bucketing value".
3844
The bucketing value expression can be omitted, in which case a concatenation of the `targetingKey` and the `flagKey` will be used.
3945

@@ -46,8 +52,10 @@ is selected.
4652
As hashing is deterministic we can be sure to get the same result every time for the same data point.
4753

4854
The `fractional` operation can be added as part of a targeting definition.
49-
The value is an array and the first element is the name of the property to use from the evaluation context.
55+
The value is an array and the first element is a nested JsonLogic rule which resolves to the hash key.
56+
This rule should typically consist of a seed concatenated with a session variable to use from the evaluation context.
5057
This value should typically be something that remains consistent for the duration of a users session (e.g. email or session ID).
58+
The seed is typically the flagKey so that experiments running across different flags are statistically independent, however, you can also specify another seed to either align or further decouple your allocations across different feature flags or use-cases.
5159
The other elements in the array are nested arrays with the first element representing a variant and the second being the percentage that this option is selected.
5260
There is no limit to the number of elements but the configured percentages must add up to 100.
5361

@@ -69,7 +77,12 @@ Flags defined as such:
6977
"state": "ENABLED",
7078
"targeting": {
7179
"fractional": [
72-
{ "var": "email" },
80+
{
81+
"cat": [
82+
{ "var": "$flagd.flagKey" },
83+
{ "var": "email" }
84+
]
85+
},
7386
[
7487
"red",
7588
50

docs/reference/specifications/custom-operations/fractional-operation-spec.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ JSON object. Below is an example of a targeting rule containing a `fractional`:
3434
"state": "ENABLED",
3535
"targeting": {
3636
"fractional": [
37-
{"var":"email"},
37+
{
38+
"cat": [
39+
{ "var": "$flagd.flagKey" },
40+
{ "var": "email" }
41+
]
42+
},
3843
[
3944
"red",
4045
50

0 commit comments

Comments
 (0)