diff --git a/grpc/flag_service.go b/grpc/flag_service.go index aae6e03..b5fc1e9 100644 --- a/grpc/flag_service.go +++ b/grpc/flag_service.go @@ -99,10 +99,10 @@ func (s *flagService) EvalFlag(_ context.Context, req *proto.EvalRequest) (*prot if err != nil { return nil, err } - value, err := sdkClient.Eval(req.GetKey(), user) - if err != nil { + value := sdkClient.Eval(req.GetKey(), user) + if value.Error != nil { var errKeyNotFound configcat.ErrKeyNotFound - if errors.As(err, &errKeyNotFound) { + if errors.As(value.Error, &errKeyNotFound) { return nil, status.Error(codes.NotFound, "feature flag or setting with key '"+req.GetKey()+"' not found") } else { return nil, status.Error(codes.Unknown, "the request failed; please check the logs for more details") diff --git a/model/eval.go b/model/eval.go index e075a4a..be818e5 100644 --- a/model/eval.go +++ b/model/eval.go @@ -7,6 +7,7 @@ import ( type EvalData struct { Value interface{} VariationId string + Error error User configcat.User } diff --git a/sdk/sdk.go b/sdk/sdk.go index 371cc0a..25613be 100644 --- a/sdk/sdk.go +++ b/sdk/sdk.go @@ -25,7 +25,7 @@ const ( ) type Client interface { - Eval(key string, user model.UserAttrs) (model.EvalData, error) + Eval(key string, user model.UserAttrs) model.EvalData EvalAll(user model.UserAttrs) map[string]model.EvalData Keys() []string GetCachedJson() *store.EntryWithEtag @@ -66,6 +66,8 @@ type client struct { func NewClient(sdkCtx *Context, log log.Logger) Client { sdkLog := log.WithLevel(sdkCtx.SDKConf.Log.GetLevel()).WithPrefix("sdk-" + sdkCtx.SdkId) + sdkCtx.StatusReporter.RegisterSdk(sdkCtx.SdkId, sdkCtx.SDKConf) + offline := sdkCtx.SDKConf.Offline.Enabled key := sdkCtx.SDKConf.Key var storage configcat.ConfigCache @@ -183,10 +185,10 @@ func (c *client) signal() { } } -func (c *client) Eval(key string, user model.UserAttrs) (model.EvalData, error) { +func (c *client) Eval(key string, user model.UserAttrs) model.EvalData { mergedUser := model.MergeUserAttrs(c.defaultAttrs, user) details := c.configCatClient.Snapshot(mergedUser).GetValueDetails(key) - return model.EvalData{Value: details.Value, VariationId: details.Data.VariationID, User: details.Data.User}, details.Data.Error + return model.EvalData{Value: details.Value, VariationId: details.Data.VariationID, User: details.Data.User, Error: details.Data.Error} } func (c *client) EvalAll(user model.UserAttrs) map[string]model.EvalData { @@ -194,7 +196,7 @@ func (c *client) EvalAll(user model.UserAttrs) map[string]model.EvalData { allDetails := c.configCatClient.Snapshot(mergedUser).GetAllValueDetails() result := make(map[string]model.EvalData, len(allDetails)) for _, details := range allDetails { - result[details.Data.Key] = model.EvalData{Value: details.Value, VariationId: details.Data.VariationID, User: details.Data.User} + result[details.Data.Key] = model.EvalData{Value: details.Value, VariationId: details.Data.VariationID, User: details.Data.User, Error: details.Data.Error} } return result } diff --git a/sdk/sdk_registrar.go b/sdk/sdk_registrar.go index 486a7d4..c2272b1 100644 --- a/sdk/sdk_registrar.go +++ b/sdk/sdk_registrar.go @@ -21,7 +21,6 @@ type registrar struct { func NewRegistrar(conf *config.Config, metricsReporter metrics.Reporter, statusReporter status.Reporter, externalCache configcat.ConfigCache, log log.Logger) Registrar { sdkClients := make(map[string]Client, len(conf.SDKs)) for key, sdkConf := range conf.SDKs { - statusReporter.RegisterSdk(key, sdkConf) sdkClients[key] = NewClient(&Context{ SDKConf: sdkConf, MetricsReporter: metricsReporter, diff --git a/sdk/sdk_registrar_test.go b/sdk/sdk_registrar_test.go index e3898f3..934904f 100644 --- a/sdk/sdk_registrar_test.go +++ b/sdk/sdk_registrar_test.go @@ -39,13 +39,3 @@ func TestClient_Close(t *testing.T) { <-c.ctx.Done() }) } - -func TestRegistrar_Reporter(t *testing.T) { - reporter := status.NewEmptyReporter() - reg := NewRegistrar(&config.Config{ - SDKs: map[string]*config.SDKConfig{"test": {Key: "key"}}, - }, nil, reporter, nil, log.NewNullLogger()) - defer reg.Close() - - assert.NotEmpty(t, reporter.GetStatus().SDKs) -} diff --git a/sdk/sdk_test.go b/sdk/sdk_test.go index 5ac3a20..c3b3c4f 100644 --- a/sdk/sdk_test.go +++ b/sdk/sdk_test.go @@ -36,9 +36,9 @@ func TestSdk_Signal(t *testing.T) { client := NewClient(ctx, log.NewNullLogger()) defer client.Close() sub := client.SubConfigChanged("id") - data, err := client.Eval("flag", nil) + data := client.Eval("flag", nil) j := client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.True(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":true,"s":null,"i":null,"d":null},"t":0,"r":[],"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, fmt.Sprintf("%x", sha1.Sum(j.ConfigJson)), j.ETag) @@ -51,9 +51,9 @@ func TestSdk_Signal(t *testing.T) { utils.WithTimeout(2*time.Second, func() { <-sub }) - data, err = client.Eval("flag", nil) + data = client.Eval("flag", nil) j = client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.False(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":false,"s":null,"i":null,"d":null},"t":0,"r":[],"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, fmt.Sprintf("%x", sha1.Sum(j.ConfigJson)), j.ETag) @@ -110,9 +110,9 @@ func TestSdk_Signal_Refresh(t *testing.T) { client := NewClient(ctx, log.NewNullLogger()) defer client.Close() sub := client.SubConfigChanged("id") - data, err := client.Eval("flag", nil) + data := client.Eval("flag", nil) j := client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.True(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":true,"s":null,"i":null,"d":null},"t":0,"r":[],"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, fmt.Sprintf("%x", sha1.Sum(j.ConfigJson)), j.ETag) @@ -126,10 +126,11 @@ func TestSdk_Signal_Refresh(t *testing.T) { utils.WithTimeout(2*time.Second, func() { <-sub }) - data, err = client.Eval("flag", nil) + data = client.Eval("flag", nil) j = client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.False(t, data.Value.(bool)) + assert.Nil(t, data.Error) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":false,"s":null,"i":null,"d":null},"t":0,"r":[],"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, fmt.Sprintf("%x", sha1.Sum(j.ConfigJson)), j.ETag) } @@ -143,10 +144,11 @@ func TestSdk_BadConfig(t *testing.T) { ctx := newTestSdkContext(&config.SDKConfig{BaseUrl: srv.URL, Key: key, Log: config.LogConfig{Level: "debug"}}, nil) client := NewClient(ctx, log.NewDebugLogger()) defer client.Close() - data, err := client.Eval("flag", nil) + data := client.Eval("flag", nil) j := client.GetCachedJson() - assert.Error(t, err) + assert.Error(t, data.Error) assert.Nil(t, data.Value) + assert.NotNil(t, data.Error) assert.Equal(t, `{"f":null,"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, utils.GenerateEtag(j.ConfigJson), j.ETag) } @@ -161,13 +163,14 @@ func TestSdk_BadConfig_WithCache(t *testing.T) { cacheKey := configcatcache.ProduceCacheKey(key, configcatcache.ConfigJSONName, configcatcache.ConfigJSONCacheVersion) cacheEntry := configcatcache.CacheSegmentsToBytes(time.Now(), "etag", []byte(`{"f":{"flag":{"a":"","i":"v_flag","v":{"b":true},"t":0}}}`)) err := s.Set(cacheKey, string(cacheEntry)) + assert.NoError(t, err) ctx := newTestSdkContext(&config.SDKConfig{BaseUrl: srv.URL, Key: key, Log: config.LogConfig{Level: "debug"}}, newRedisCache(s.Addr())) client := NewClient(ctx, log.NewDebugLogger()) defer client.Close() - data, err := client.Eval("flag", nil) + data := client.Eval("flag", nil) j := client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.True(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":true},"t":0}}}`, string(j.ConfigJson)) assert.Equal(t, "etag", j.ETag) @@ -179,9 +182,9 @@ func TestSdk_Signal_Offline_File_Watch(t *testing.T) { client := NewClient(ctx, log.NewNullLogger()) defer client.Close() sub := client.SubConfigChanged("id") - data, err := client.Eval("flag", nil) + data := client.Eval("flag", nil) j := client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.True(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":true,"s":null,"i":null,"d":null},"t":0,"r":null,"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, fmt.Sprintf("W/\"%s\"", utils.FastHashHex(j.ConfigJson)), j.ETag) @@ -190,9 +193,9 @@ func TestSdk_Signal_Offline_File_Watch(t *testing.T) { utils.WithTimeout(2*time.Second, func() { <-sub }) - data, err = client.Eval("flag", nil) + data = client.Eval("flag", nil) j = client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.False(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":false,"s":null,"i":null,"d":null},"t":0,"r":null,"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, utils.GenerateEtag(j.ConfigJson), j.ETag) @@ -205,9 +208,9 @@ func TestSdk_Signal_Offline_Poll_Watch(t *testing.T) { client := NewClient(ctx, log.NewNullLogger()) defer client.Close() sub := client.SubConfigChanged("id") - data, err := client.Eval("flag", nil) + data := client.Eval("flag", nil) j := client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.True(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":true,"s":null,"i":null,"d":null},"t":0,"r":null,"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, fmt.Sprintf("W/\"%s\"", utils.FastHashHex(j.ConfigJson)), j.ETag) @@ -216,9 +219,9 @@ func TestSdk_Signal_Offline_Poll_Watch(t *testing.T) { utils.WithTimeout(2*time.Second, func() { <-sub }) - data, err = client.Eval("flag", nil) + data = client.Eval("flag", nil) j = client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.False(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":false,"s":null,"i":null,"d":null},"t":0,"r":null,"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, utils.GenerateEtag(j.ConfigJson), j.ETag) @@ -239,9 +242,9 @@ func TestSdk_Signal_Offline_Redis_Watch(t *testing.T) { client := NewClient(ctx, log.NewNullLogger()) defer client.Close() sub := client.SubConfigChanged("id") - data, err := client.Eval("flag", nil) + data := client.Eval("flag", nil) j := client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.True(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":true,"s":null,"i":null,"d":null},"t":0,"r":null,"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, "etag", j.ETag) @@ -251,9 +254,9 @@ func TestSdk_Signal_Offline_Redis_Watch(t *testing.T) { utils.WithTimeout(2*time.Second, func() { <-sub }) - data, err = client.Eval("flag", nil) + data = client.Eval("flag", nil) j = client.GetCachedJson() - assert.NoError(t, err) + assert.NoError(t, data.Error) assert.False(t, data.Value.(bool)) assert.Equal(t, `{"f":{"flag":{"a":"","i":"v_flag","v":{"b":false,"s":null,"i":null,"d":null},"t":0,"r":null,"p":null}},"s":null,"p":null}`, string(j.ConfigJson)) assert.Equal(t, "etag2", j.ETag) @@ -301,6 +304,8 @@ func TestSdk_EvalAll(t *testing.T) { assert.Equal(t, 2, len(details)) assert.Equal(t, "v1", details["flag1"].Value) assert.Equal(t, "v2", details["flag2"].Value) + assert.Nil(t, details["flag1"].Error) + assert.Nil(t, details["flag2"].Error) } func TestSdk_Keys(t *testing.T) { @@ -344,7 +349,7 @@ func TestSdk_EvalStatsReporter(t *testing.T) { client := NewClient(ctx, log.NewNullLogger()) defer client.Close() - _, _ = client.Eval("flag1", model.UserAttrs{"e": "h"}) + _ = client.Eval("flag1", model.UserAttrs{"e": "h"}) var event *statistics.EvalEvent utils.WithTimeout(2*time.Second, func() { @@ -369,7 +374,7 @@ func TestSdk_DefaultAttrs(t *testing.T) { client := NewClient(ctx, log.NewNullLogger()) defer client.Close() - evalData, _ := client.Eval("flag1", model.UserAttrs{"e": "h"}) + evalData := client.Eval("flag1", model.UserAttrs{"e": "h"}) assert.Equal(t, model.UserAttrs{"a": "g", "c": "d", "e": "h"}, evalData.User.(model.UserAttrs)) } @@ -427,11 +432,24 @@ func TestSdk_IsInValidState_EmptyCache_False(t *testing.T) { assert.False(t, client.IsInValidState()) } +func TestSdk_StatusReporter(t *testing.T) { + reporter := status.NewEmptyReporter() + ctx := newTestSdkContextWithReporter(&config.SDKConfig{BaseUrl: "https://localhost", Key: configcattest.RandomSDKKey()}, nil, reporter) + client := NewClient(ctx, log.NewDebugLogger()) + defer client.Close() + + assert.NotEmpty(t, reporter.GetStatus().SDKs) +} + func newTestSdkContext(conf *config.SDKConfig, externalCache configcat.ConfigCache) *Context { + return newTestSdkContextWithReporter(conf, externalCache, status.NewEmptyReporter()) +} + +func newTestSdkContextWithReporter(conf *config.SDKConfig, externalCache configcat.ConfigCache, reporter status.Reporter) *Context { return &Context{ SDKConf: conf, ProxyConf: &config.HttpProxyConfig{}, - StatusReporter: status.NewEmptyReporter(), + StatusReporter: reporter, MetricsReporter: nil, EvalReporter: nil, SdkId: "test", diff --git a/stream/channel.go b/stream/channel.go index 5f1e119..30f0da4 100644 --- a/stream/channel.go +++ b/stream/channel.go @@ -42,7 +42,7 @@ func createChannel(established *connEstablished, sdkClient sdk.Client) channel { } return &allFlagsChannel{connectionHolder: connectionHolder{user: established.user}, lastPayload: payloads} } else { - val, _ := sdkClient.Eval(established.key, established.user) + val := sdkClient.Eval(established.key, established.user) payload := model.PayloadFromEvalData(&val) return &singleFlagChannel{connectionHolder: connectionHolder{user: established.user}, lastPayload: &payload} } @@ -58,8 +58,8 @@ func (af *allFlagsChannel) LastPayload() interface{} { func (sf *singleFlagChannel) Notify(sdkClient sdk.Client, key string) int { sent := 0 - val, err := sdkClient.Eval(key, sf.user) - if err != nil { + val := sdkClient.Eval(key, sf.user) + if val.Error != nil { return 0 } if sf.lastPayload == nil || val.Value != sf.lastPayload.Value { diff --git a/web/api/api.go b/web/api/api.go index be89477..379d5b9 100644 --- a/web/api/api.go +++ b/web/api/api.go @@ -41,10 +41,10 @@ func (s *Server) Eval(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), code) return } - eval, err := sdkClient.Eval(evalReq.Key, evalReq.User) - if err != nil { + eval := sdkClient.Eval(evalReq.Key, evalReq.User) + if eval.Error != nil { var errKeyNotFound configcat.ErrKeyNotFound - if errors.As(err, &errKeyNotFound) { + if errors.As(eval.Error, &errKeyNotFound) { http.Error(w, "feature flag or setting with key '"+evalReq.Key+"' not found", http.StatusBadRequest) } else { http.Error(w, "the request failed; please check the logs for more details", http.StatusInternalServerError)