Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(relay-proxy): Add the possibility to specify in the evaluation context which flag to evaluate in bulk evaluation flags #2171

Merged
merged 6 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion cmd/relayproxy/controller/all_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package controller

import (
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config"
"github.com/thomaspoignant/go-feature-flag/internal/flagstate"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"net/http"
Expand Down Expand Up @@ -59,7 +60,15 @@ func (h *allFlags) Handler(c echo.Context) error {
tracer := otel.GetTracerProvider().Tracer(config.OtelTracerName)
_, span := tracer.Start(c.Request().Context(), "AllFlagsState")
defer span.End()
allFlags := h.goFF.AllFlagsState(evaluationCtx)

var allFlags flagstate.AllFlags
if len(evaluationCtx.ExtractGOFFProtectedFields().FlagList) > 0 {
// if we have a list of flags to evaluate in the evaluation context, we evaluate only those flags.
allFlags = h.goFF.GetFlagStates(evaluationCtx, evaluationCtx.ExtractGOFFProtectedFields().FlagList)
} else {
allFlags = h.goFF.AllFlagsState(evaluationCtx)
}

span.SetAttributes(
attribute.Bool("AllFlagsState.valid", allFlags.IsValid()),
attribute.Int("AllFlagsState.numberEvaluation", len(allFlags.GetFlags())),
Expand Down
11 changes: 11 additions & 0 deletions cmd/relayproxy/controller/all_flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ func Test_all_flag_Handler(t *testing.T) {
errorCode: http.StatusBadRequest,
},
},
{
name: "specify flags in evaluation context",
args: args{
bodyFile: "../testdata/controller/all_flags/valid_request_specify_flags.json",
configFlagsLocation: configFlagsLocation,
},
want: want{
httpCode: http.StatusOK,
bodyFile: "../testdata/controller/all_flags/valid_response_specify_flags.json",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
9 changes: 8 additions & 1 deletion cmd/relayproxy/ofrep/evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/model"
"github.com/thomaspoignant/go-feature-flag/ffcontext"
"github.com/thomaspoignant/go-feature-flag/internal/flag"
"github.com/thomaspoignant/go-feature-flag/internal/flagstate"
"github.com/thomaspoignant/go-feature-flag/internal/utils"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
Expand Down Expand Up @@ -173,7 +174,13 @@ func (h *EvaluateCtrl) BulkEvaluate(c echo.Context) error {
_, span := tracer.Start(c.Request().Context(), "AllFlagsState")
defer span.End()

allFlagsResp := h.goFF.AllFlagsState(evalCtx)
var allFlagsResp flagstate.AllFlags
if len(evalCtx.ExtractGOFFProtectedFields().FlagList) > 0 {
// if we have a list of flags to evaluate in the evaluation context, we evaluate only those flags.
allFlagsResp = h.goFF.GetFlagStates(evalCtx, evalCtx.ExtractGOFFProtectedFields().FlagList)
} else {
allFlagsResp = h.goFF.AllFlagsState(evalCtx)
}
for key, val := range allFlagsResp.GetFlags() {
value := val.Value
if val.Reason == flag.ReasonError {
Expand Down
11 changes: 11 additions & 0 deletions cmd/relayproxy/ofrep/evaluate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ func Test_Bulk_Evaluation(t *testing.T) {
bodyFile: "../testdata/ofrep/responses/valid_response.json",
},
},
{
name: "specify flag list in context",
args: args{
bodyFile: "../testdata/ofrep/valid_request_specify_flags.json",
configFlagsLocation: configFlagsLocation,
},
want: want{
httpCode: http.StatusOK,
bodyFile: "../testdata/ofrep/responses/valid_response_specify_flags.json",
},
},
{
name: "Invalid context",
args: args{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"user": {
"key": "a20b1cd5-7165-4e02-a279-c0c8b90a8912",
"anonymous": false,
"custom": {
"custom1": "value1",
"custom2": "value2",
"gofeatureflag":{
"flagList":["array-flag", "flag-only-for-admin"]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"flags": {
"array-flag": {
"value": [
"batmanDefault",
"supermanDefault",
"superherosDefault"
],
"timestamp": 1652273630,
"variationType": "Default",
"trackEvents": true,
"errorCode": "",
"reason": "DEFAULT"
},
"flag-only-for-admin": {
"value": false,
"timestamp": 1652273630,
"variationType": "Default",
"trackEvents": true,
"errorCode": "",
"reason": "DEFAULT"
}
},
"valid": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"flags": [
{
"key": "array-flag",
"value": [
"batmanDefault",
"supermanDefault",
"superherosDefault"
],
"reason": "DEFAULT",
"variant": "Default"
},
{
"key": "flag-only-for-admin",
"value": false,
"reason": "DEFAULT",
"variant": "Default"
}
]
}
11 changes: 11 additions & 0 deletions cmd/relayproxy/testdata/ofrep/valid_request_specify_flags.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"context": {
"company": "GO Feature Flag",
"firstname": "John",
"lastname": "Doe",
"targetingKey": "4f433951-4c8c-42b3-9f18-8c9a5ed8e9eb",
"gofeatureflag": {
"flagList": ["array-flag", "flag-only-for-admin"]
}
}
}
23 changes: 4 additions & 19 deletions ffcontext/context.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package ffcontext

import (
"time"
)

type Context interface {
// GetKey return the unique key for the context.
GetKey() string
Expand All @@ -17,10 +13,6 @@ type Context interface {
ExtractGOFFProtectedFields() GoffContextSpecifics
}

type GoffContextSpecifics struct {
CurrentDateTime *time.Time `json:"currentDateTime"`
}

// value is a type to define custom attribute.
type value map[string]interface{}

Expand Down Expand Up @@ -85,20 +77,13 @@ func (u EvaluationContext) AddCustomAttribute(name string, value interface{}) {
// ExtractGOFFProtectedFields extract the goff specific attributes from the evaluation context.
func (u EvaluationContext) ExtractGOFFProtectedFields() GoffContextSpecifics {
goff := GoffContextSpecifics{}

switch v := u.custom["gofeatureflag"].(type) {
case map[string]string:
if currentDateTimeStr, ok := v["currentDateTime"]; ok {
if currentDateTime, err := time.ParseInLocation(time.RFC3339, currentDateTimeStr, time.Local); err == nil {
goff.CurrentDateTime = &currentDateTime
}
}
goff.addCurrentDateTime(v["currentDateTime"])
goff.addListFlags(v["flagList"])
case map[string]interface{}:
if currentDateTimeStr, ok := v["currentDateTime"].(string); ok {
if currentDateTime, err := time.ParseInLocation(time.RFC3339, currentDateTimeStr, time.Local); err == nil {
goff.CurrentDateTime = &currentDateTime
}
}
goff.addCurrentDateTime(v["currentDateTime"])
goff.addListFlags(v["flagList"])
case GoffContextSpecifics:
return v
}
Expand Down
49 changes: 49 additions & 0 deletions ffcontext/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@ func Test_ExtractGOFFProtectedFields(t *testing.T) {
CurrentDateTime: testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)),
},
},
{
name: "context goff specifics as map[string]interface and date as time.Time",
ctx: ffcontext.NewEvaluationContextBuilder("my-key").AddCustom("gofeatureflag", map[string]interface{}{
"currentDateTime": time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC),
}).Build(),
want: ffcontext.GoffContextSpecifics{
CurrentDateTime: testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)),
},
},
{
name: "context goff specifics as map[string]interface and date as *time.Time",
ctx: ffcontext.NewEvaluationContextBuilder("my-key").AddCustom("gofeatureflag", map[string]interface{}{
"currentDateTime": testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)),
}).Build(),
want: ffcontext.GoffContextSpecifics{
CurrentDateTime: testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)),
},
},
{
name: "context goff specifics as map[string]interface",
ctx: ffcontext.NewEvaluationContextBuilder("my-key").AddCustom("gofeatureflag", map[string]interface{}{
Expand Down Expand Up @@ -92,6 +110,37 @@ func Test_ExtractGOFFProtectedFields(t *testing.T) {
CurrentDateTime: testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)),
},
},
{
name: "context goff specifics as GoffContextSpecifics type contains flagList",
ctx: ffcontext.NewEvaluationContextBuilder("my-key").AddCustom("gofeatureflag", ffcontext.GoffContextSpecifics{
CurrentDateTime: testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)),
FlagList: []string{"flag1", "flag2"},
}).Build(),
want: ffcontext.GoffContextSpecifics{
CurrentDateTime: testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)),
FlagList: []string{"flag1", "flag2"},
},
},
{
name: "context goff specifics as map[string]interface type contains flagList",
ctx: ffcontext.NewEvaluationContextBuilder("my-key").AddCustom("gofeatureflag", map[string]interface{}{
"currentDateTime": testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)).Format(time.RFC3339),
"flagList": []string{"flag1", "flag2"},
}).Build(),
want: ffcontext.GoffContextSpecifics{
CurrentDateTime: testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)),
FlagList: []string{"flag1", "flag2"},
},
},
{
name: "context goff specifics only flagList",
ctx: ffcontext.NewEvaluationContextBuilder("my-key").AddCustom("gofeatureflag", map[string]interface{}{
"flagList": []string{"flag1", "flag2"},
}).Build(),
want: ffcontext.GoffContextSpecifics{
FlagList: []string{"flag1", "flag2"},
},
},
}

for _, tt := range tests {
Expand Down
42 changes: 42 additions & 0 deletions ffcontext/goff_context_specifics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package ffcontext

import "time"

type GoffContextSpecifics struct {
// CurrentDateTime is the current date time to use for the evaluation.
CurrentDateTime *time.Time `json:"currentDateTime"`
// FlagList is the list of flags to evaluate in a bulk evaluation.
FlagList []string `json:"flagList"`
}

// addCurrentDateTime adds the current date time to the context.
// This function formats the current date time to RFC3339 format.
func (g *GoffContextSpecifics) addCurrentDateTime(currentDateTime any) {
switch value := currentDateTime.(type) {
case *time.Time:
g.CurrentDateTime = value
case time.Time:
g.CurrentDateTime = &value
case string:
if currentDateTime, err := time.ParseInLocation(time.RFC3339, value, time.Local); err == nil {
g.CurrentDateTime = &currentDateTime
}
return
default:
return
}
}

// addListFlags adds the list of flags to evaluate in a bulk evaluation.
func (g *GoffContextSpecifics) addListFlags(flagList any) {
if value, ok := flagList.([]string); ok {
g.FlagList = value
}
if value, ok := flagList.([]interface{}); ok {
for _, val := range value {
if valAsString, ok := val.(string); ok {
g.FlagList = append(g.FlagList, valAsString)

Check warning on line 38 in ffcontext/goff_context_specifics.go

View check run for this annotation

Codecov / codecov/patch

ffcontext/goff_context_specifics.go#L36-L38

Added lines #L36 - L38 were not covered by tests
}
}
}
}
47 changes: 47 additions & 0 deletions internal/flagstate/flag_state.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package flagstate

import (
"github.com/thomaspoignant/go-feature-flag/ffcontext"
"github.com/thomaspoignant/go-feature-flag/internal/flag"
"time"
)

// FlagState represents the state of an individual feature flag, with regard to a specific user, when it was called.
Expand All @@ -15,3 +17,48 @@ type FlagState struct {
Reason flag.ResolutionReason `json:"reason"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}

func FromFlagEvaluation(key string, evaluationCtx ffcontext.Context,
flagCtx flag.Context, currentFlag flag.Flag) FlagState {
flagValue, resolutionDetails := currentFlag.Value(key, evaluationCtx, flagCtx)

// if the flag is disabled, we are ignoring it.
if resolutionDetails.Reason == flag.ReasonDisabled {
return FlagState{
Timestamp: time.Now().Unix(),
TrackEvents: currentFlag.IsTrackEvents(),
Failed: resolutionDetails.ErrorCode != "",
ErrorCode: resolutionDetails.ErrorCode,
Reason: resolutionDetails.Reason,
Metadata: resolutionDetails.Metadata,
}
}

switch v := flagValue; v.(type) {
case int, float64, bool, string, []interface{}, map[string]interface{}:
return FlagState{
Value: v,
Timestamp: time.Now().Unix(),
VariationType: resolutionDetails.Variant,
TrackEvents: currentFlag.IsTrackEvents(),
Failed: resolutionDetails.ErrorCode != "",
ErrorCode: resolutionDetails.ErrorCode,
Reason: resolutionDetails.Reason,
Metadata: resolutionDetails.Metadata,
}

default:
defaultVariationName := flag.VariationSDKDefault
defaultVariationValue := currentFlag.GetVariationValue(defaultVariationName)
return FlagState{
Value: defaultVariationValue,
Timestamp: time.Now().Unix(),
VariationType: defaultVariationName,
TrackEvents: currentFlag.IsTrackEvents(),
Failed: true,
ErrorCode: flag.ErrorCodeTypeMismatch,
Reason: flag.ReasonError,
Metadata: resolutionDetails.Metadata,
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ describe('Provider tests', () => {
});
goffClient = OpenFeature.getClient('my-app')
await OpenFeature.setProviderAndWait('my-app', goFeatureFlagProvider);
OpenFeature.setContext({
gofeatureflag:{
flagList: ["flag1", "flag2"]
}
})

userCtx = {
targetingKey: 'd45e303a-38c2-11ed-a261-0242ac120002', // user unique identifier (mandatory)
Expand Down
Loading
Loading