From 0202715705f30f002bde31d0797ae4d3219579f3 Mon Sep 17 00:00:00 2001 From: Erdi Rowlands Date: Fri, 6 Oct 2023 13:40:41 +0100 Subject: [PATCH] FFM-9486 Sdd SDK Codes (#130) --- analyticsservice/analytics.go | 7 +- client/client.go | 76 ++++++++++++++++----- client/client_test.go | 125 ++++++++++++++++++++++------------ client/errors.go | 7 +- evaluation/evaluator.go | 43 ++++++------ evaluation/evaluator_test.go | 42 ++++++++---- sdk_codes/sdk_codes.go | 31 +++++++++ stream/sse.go | 5 +- tests/evaluator_test.go | 8 +-- 9 files changed, 233 insertions(+), 111 deletions(-) create mode 100644 sdk_codes/sdk_codes.go diff --git a/analyticsservice/analytics.go b/analyticsservice/analytics.go index bb91dc57..231682b1 100644 --- a/analyticsservice/analytics.go +++ b/analyticsservice/analytics.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/harness/ff-golang-server-sdk/sdk_codes" "strconv" "sync" "time" @@ -72,6 +73,7 @@ func NewAnalyticsService(timeout time.Duration, logger logger.Logger) *Analytics // Start starts the client and timer to send analytics func (as *AnalyticsService) Start(ctx context.Context, client *metricsclient.ClientWithResponsesInterface, environmentID string) { + as.logger.Infof("%s Metrics started", sdk_codes.MetricsStarted) as.metricsClient = client as.environmentID = environmentID go as.startTimer(ctx) @@ -83,6 +85,7 @@ func (as *AnalyticsService) startTimer(ctx context.Context) { case <-time.After(as.timeout): as.sendDataAndResetCache(ctx) case <-ctx.Done(): + as.logger.Infof("%s Metrics stopped", sdk_codes.MetricsStopped) return } } @@ -273,11 +276,11 @@ func (as *AnalyticsService) sendDataAndResetCache(ctx context.Context) { return } if resp.StatusCode() != 200 { - as.logger.Warn("Non 200 response from metrics server: %d", resp.StatusCode()) + as.logger.Warnf("%s Non 200 response from metrics server: %d", sdk_codes.MetricsSendFail, resp.StatusCode()) return } - as.logger.Debug("Metrics sent to server") + as.logger.Debugf("%s Metrics sent to server", sdk_codes.MetricsSendSuccess) } else { as.logger.Warn("metrics client is not set") } diff --git a/client/client.go b/client/client.go index 5cc748b7..97d82d6f 100644 --- a/client/client.go +++ b/client/client.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/harness/ff-golang-server-sdk/sdk_codes" "log" "math/rand" "net/http" @@ -90,7 +91,7 @@ func NewCfClient(sdkKey string, options ...ConfigOption) (*CfClient, error) { } if sdkKey == "" { - config.Logger.Errorf("Initialization failed: SDK Key cannot be empty. Please provide a valid SDK Key to initialize the client.") + config.Logger.Errorf("%s Initialization failed: SDK Key cannot be empty. Please provide a valid SDK Key to initialize the client.", sdk_codes.InitMissingKey) return client, EmptySDKKeyError } @@ -113,10 +114,13 @@ func NewCfClient(sdkKey string, options ...ConfigOption) (*CfClient, error) { client.start() if config.waitForInitialized { + config.Logger.Infof("%s The SDK is waiting for initialization to complete'", sdk_codes.InitWaiting) + var initErr error select { case <-client.initialized: + config.Logger.Infof("%s The SDK has successfully initialized'", sdk_codes.InitSuccess) return client, nil case err := <-client.initializedErr: initErr = err @@ -144,6 +148,7 @@ func (c *CfClient) start() { go func() { if err := c.initAuthentication(context.Background()); err != nil { + c.config.Logger.Error("%s The SDK has failed to initialize due to an authentication error: %v' ", sdk_codes.InitAuthError, err) c.initializedErr <- err } }() @@ -240,7 +245,7 @@ func (c *CfClient) streamConnect(ctx context.Context) { sseClient := sse.NewClient(fmt.Sprintf("%s/stream?cluster=%s", c.config.url, c.clusterIdentifier)) streamErr := func() { - c.config.Logger.Warn("Stream disconnected. Swapping to polling mode") + c.config.Logger.Warnf("%s Stream disconnected. Swapping to polling mode", sdk_codes.StreamDisconnected) c.mux.RLock() defer c.mux.RUnlock() c.streamConnected = false @@ -276,25 +281,26 @@ func (c *CfClient) initAuthentication(ctx context.Context) error { for { err := c.authenticate(ctx) if err == nil { + c.config.Logger.Infof("%s Authenticated successfully'", sdk_codes.AuthSuccess) return nil } var nonRetryableAuthError NonRetryableAuthError if errors.As(err, &nonRetryableAuthError) { - c.config.Logger.Error("Authentication failed with a non-retryable error: '%s %s' Default variations will now be served", nonRetryableAuthError.StatusCode, nonRetryableAuthError.Message) + c.config.Logger.Error("%s Authentication failed with a non-retryable error: '%s %s' Default variations will now be served", sdk_codes.AuthFailed, nonRetryableAuthError.StatusCode, nonRetryableAuthError.Message) return err } // -1 is the default maxAuthRetries option and indicates there should be no max attempts if c.config.maxAuthRetries != -1 && attempts >= c.config.maxAuthRetries { - c.config.Logger.Errorf("Authentication failed with error: '%s'. Exceeded max attempts: '%v'.", err, c.config.maxAuthRetries) + c.config.Logger.Errorf("%s Authentication failed with error: '%s'. Exceeded max attempts: '%v'.", sdk_codes.AuthExceededRetries, err, c.config.maxAuthRetries) return err } jitter := time.Duration(rand.Float64() * float64(currentDelay)) delayWithJitter := currentDelay + jitter - c.config.Logger.Errorf("Authentication failed with error: '%s'. Retrying in %v.", err, delayWithJitter) + c.config.Logger.Errorf("%s Authentication failed with error: '%s'. Retrying in %v.", sdk_codes.AuthAttempt, err, delayWithJitter) c.config.sleeper.Sleep(delayWithJitter) currentDelay *= time.Duration(factor) @@ -414,10 +420,12 @@ func (c *CfClient) makeTicker(interval uint) *time.Ticker { func (c *CfClient) pullCronJob(ctx context.Context) { poll := func() { c.mux.RLock() + c.config.Logger.Infof("%s Polling started, interval: %v", sdk_codes.PollStart, c.config.pullInterval) if !c.streamConnected { ok := c.retrieve(ctx) // we should only try and start the stream after the poll succeeded to make sure we get the latest changes if ok && c.config.enableStream { + c.config.Logger.Infof("%s Polling Stopped", sdk_codes.PollStop) // here stream is enabled but not connected, so we attempt to reconnect c.config.Logger.Info("Attempting to start stream") c.streamConnect(ctx) @@ -437,6 +445,8 @@ func (c *CfClient) pullCronJob(ctx context.Context) { select { case <-ctx.Done(): pullingTicker.Stop() + c.config.Logger.Infof("%s Polling stopped", sdk_codes.PollStop) + c.config.Logger.Infof("%s Stream stopped", sdk_codes.StreamStop) return case <-pullingTicker.C: poll() @@ -508,14 +518,18 @@ func (c *CfClient) setAnalyticsServiceClient(ctx context.Context) { } // BoolVariation returns the value of a boolean feature flag for a given target. -// // Returns defaultValue if there is an error or if the flag doesn't exist func (c *CfClient) BoolVariation(key string, target *evaluation.Target, defaultValue bool) (bool, error) { if !c.initializedBool { - c.config.Logger.Info("Error when calling BoolVariation and returning default variation: 'Client is not initialized'") + c.config.Logger.Infof("%s Error while evaluating boolean flag and returning default variation: 'Client is not initialized'", sdk_codes.EvaluationFailed) return defaultValue, fmt.Errorf("%w: Client is not initialized", DefaultVariationReturnedError) } - value := c.evaluator.BoolVariation(key, target, defaultValue) + value, err := c.evaluator.BoolVariation(key, target, defaultValue) + if err != nil { + c.config.Logger.Infof("%s Error while evaluating boolean flag and returning default variation '%s', err: %v", sdk_codes.EvaluationFailed, key, err) + return value, fmt.Errorf("%w: `%v`", DefaultVariationReturnedError, err) + } + c.config.Logger.Debugf("%s Evaluated boolean flag successfully: '%s'", sdk_codes.EvaluationSuccess, key) return value, nil } @@ -524,10 +538,15 @@ func (c *CfClient) BoolVariation(key string, target *evaluation.Target, defaultV // Returns defaultValue if there is an error or if the flag doesn't exist func (c *CfClient) StringVariation(key string, target *evaluation.Target, defaultValue string) (string, error) { if !c.initializedBool { - c.config.Logger.Info("Error when calling StringVariation and returning default variation: 'Client is not initialized'") + c.config.Logger.Infof("%s Error while evaluating string flag and returning default variation: 'Client is not initialized'", sdk_codes.EvaluationFailed) return defaultValue, fmt.Errorf("%w: Client is not initialized", DefaultVariationReturnedError) } - value := c.evaluator.StringVariation(key, target, defaultValue) + value, err := c.evaluator.StringVariation(key, target, defaultValue) + if err != nil { + c.config.Logger.Infof("%s Error while evaluating string flag '%s', err: %v", sdk_codes.EvaluationFailed, key, err) + return value, fmt.Errorf("%w: `%v`", DefaultVariationReturnedError, err) + } + c.config.Logger.Debugf("%s Evaluated string flag successfully: '%s'", sdk_codes.EvaluationSuccess, key) return value, nil } @@ -536,10 +555,15 @@ func (c *CfClient) StringVariation(key string, target *evaluation.Target, defaul // Returns defaultValue if there is an error or if the flag doesn't exist func (c *CfClient) IntVariation(key string, target *evaluation.Target, defaultValue int64) (int64, error) { if !c.initializedBool { - c.config.Logger.Info("Error when calling IntVariation and returning default variation: 'Client is not initialized'") + c.config.Logger.Infof("%s Error while evaluating int flag and returning default variation: 'Client is not initialized'", sdk_codes.EvaluationFailed) return defaultValue, fmt.Errorf("%w: Client is not initialized", DefaultVariationReturnedError) } - value := c.evaluator.IntVariation(key, target, int(defaultValue)) + value, err := c.evaluator.IntVariation(key, target, int(defaultValue)) + if err != nil { + c.config.Logger.Infof("%s Error while evaluating int flag '%s', err: %v", sdk_codes.EvaluationFailed, key, err) + return int64(value), fmt.Errorf("%w: `%v`", DefaultVariationReturnedError, err) + } + c.config.Logger.Debugf("%s Evaluated int flag successfully: '%s'", sdk_codes.EvaluationSuccess, key) return int64(value), nil } @@ -548,10 +572,15 @@ func (c *CfClient) IntVariation(key string, target *evaluation.Target, defaultVa // Returns defaultValue if there is an error or if the flag doesn't exist func (c *CfClient) NumberVariation(key string, target *evaluation.Target, defaultValue float64) (float64, error) { if !c.initializedBool { - c.config.Logger.Info("Error when calling NumberVariation and returning default variation: 'Client is not initialized'") + c.config.Logger.Infof("%s Error while number number flag and returning default variation: 'Client is not initialized'", sdk_codes.EvaluationFailed) return defaultValue, fmt.Errorf("%w: Client is not initialized", DefaultVariationReturnedError) } - value := c.evaluator.NumberVariation(key, target, defaultValue) + value, err := c.evaluator.NumberVariation(key, target, defaultValue) + if err != nil { + c.config.Logger.Infof("%s Error while evaluating number flag '%s', err: %v", sdk_codes.EvaluationFailed, key, err) + return value, fmt.Errorf("%w: `%v`", DefaultVariationReturnedError, err) + } + c.config.Logger.Debugf("%s Evaluated number flag successfully: '%s'", sdk_codes.EvaluationSuccess, key) return value, nil } @@ -561,10 +590,15 @@ func (c *CfClient) NumberVariation(key string, target *evaluation.Target, defaul // Returns defaultValue if there is an error or if the flag doesn't exist func (c *CfClient) JSONVariation(key string, target *evaluation.Target, defaultValue types.JSON) (types.JSON, error) { if !c.initializedBool { - c.config.Logger.Info("Error when calling JSONVariation and returning default variation: 'Client is not initialized'") + c.config.Logger.Infof("%s Error while evaluating json flag and returning default variation: 'Client is not initialized'", sdk_codes.EvaluationFailed) return defaultValue, fmt.Errorf("%w: Client is not initialized", DefaultVariationReturnedError) } - value := c.evaluator.JSONVariation(key, target, defaultValue) + value, err := c.evaluator.JSONVariation(key, target, defaultValue) + if err != nil { + c.config.Logger.Infof("%s Error while evaluating json flag '%s', err: %v", sdk_codes.EvaluationFailed, key, err) + return value, fmt.Errorf("%w: `%v`", DefaultVariationReturnedError, err) + } + c.config.Logger.Debugf("%s Evaluated json flag successfully: '%s'", sdk_codes.EvaluationSuccess, key) return value, nil } @@ -577,13 +611,21 @@ func (c *CfClient) Close() error { if c.stopped.get() { return errors.New("client already closed") } + c.config.Logger.Infof("%s Closing SDK", sdk_codes.CloseStarted) close(c.stop) c.stopped.set(true) + + // This flag is used by `IsInitialized` so set to true. + c.initializedBoolLock.Lock() + c.initializedBool = false + c.initializedBoolLock.Unlock() + c.config.Logger.Infof("%s SDK Closed successfully", sdk_codes.CloseSuccess) + return nil } -// Environment returns environment based on authenticated SDK key +// Environment returns environment based on authenticated SDK flagIdentifier func (c *CfClient) Environment() string { return c.environmentID } diff --git a/client/client_test.go b/client/client_test.go index d28d2974..39ff3100 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -21,7 +21,7 @@ import ( const ( ValidSDKKey = "27bed8d2-2610-462b-90eb-d80fd594b623" EmptySDKKey = "" - InvaliDSDKKey = "an invalid key" + InvaliDSDKKey = "an invalid flagIdentifier" URL = "http://localhost/api/1.0" //nolint @@ -115,7 +115,7 @@ func TestCfClient_NewClient(t *testing.T) { err: nil, }, { - name: "Synchronous client: Empty SDK key fails to initialize", + name: "Synchronous client: Empty SDK flagIdentifier fails to initialize", newClientFunc: func() (*CfClient, error) { return newClient(http.DefaultClient, EmptySDKKey, WithWaitForInitialized(true)) }, @@ -129,7 +129,7 @@ func TestCfClient_NewClient(t *testing.T) { }, mockResponder: func() { bodyString := `{ - "message": "invalid key or target provided", + "message": "invalid flagIdentifier or target provided", "code": "401" }` authErrorResponse := AuthResponseDetailed(401, "401", bodyString) @@ -137,7 +137,7 @@ func TestCfClient_NewClient(t *testing.T) { }, err: NonRetryableAuthError{ StatusCode: "401", - Message: "invalid key or target provided", + Message: "invalid flagIdentifier or target provided", }, }, { @@ -275,7 +275,7 @@ func TestCfClient_NewClient(t *testing.T) { err: nil, }, { - name: "Asynchronous client: Empty SDK key, times out waiting", + name: "Asynchronous client: Empty SDK flagIdentifier, times out waiting", newClientFunc: func() (*CfClient, error) { client, err := newClient(http.DefaultClient, EmptySDKKey, WithSleeper(test_helpers.MockSleeper{})) if ok, err := client.IsInitialized(); !ok { @@ -297,7 +297,7 @@ func TestCfClient_NewClient(t *testing.T) { }, mockResponder: func() { bodyString := `{ - "message": "invalid key or target provided", + "message": "invalid flagIdentifier or target provided", "code": "401" }` authErrorResponse := AuthResponseDetailed(401, "401", bodyString) @@ -379,18 +379,19 @@ func TestCfClient_BoolVariation(t *testing.T) { defaultValue bool } tests := []struct { - name string - args args - want bool - wantErr bool + name string + args args + want bool + wantErr bool + expectedErr error }{ - {"Test Invalid Flag Name returns default value", args{"MadeUpIDontExist", target, false}, false, false}, - {"Test Default True Flag when On returns true", args{"TestTrueOn", target, false}, true, false}, - {"Test Default True Flag when Off returns false", args{"TestTrueOff", target, true}, false, false}, - {"Test Default False Flag when On returns false", args{"TestTrueOn", target, false}, true, false}, - {"Test Default False Flag when Off returns true", args{"TestTrueOff", target, true}, false, false}, - {"Test Default True Flag when Pre-Req is False returns false", args{"TestTrueOnWithPreReqFalse", target, true}, false, false}, - {"Test Default True Flag when Pre-Req is True returns true", args{"TestTrueOnWithPreReqTrue", target, true}, true, false}, + {"Test Invalid Flag Name returns default value", args{"MadeUpIDontExist", target, false}, false, true, DefaultVariationReturnedError}, + {"Test Default True Flag when On returns true", args{"TestTrueOn", target, false}, true, false, nil}, + {"Test Default True Flag when Off returns false", args{"TestTrueOff", target, true}, false, false, nil}, + {"Test Default False Flag when On returns false", args{"TestTrueOn", target, false}, true, false, nil}, + {"Test Default False Flag when Off returns true", args{"TestTrueOff", target, true}, false, false, nil}, + {"Test Default True Flag when Pre-Req is False returns false", args{"TestTrueOnWithPreReqFalse", target, true}, false, false, nil}, + {"Test Default True Flag when Pre-Req is True returns true", args{"TestTrueOnWithPreReqTrue", target, true}, true, false, nil}, } for _, tt := range tests { test := tt @@ -400,6 +401,9 @@ func TestCfClient_BoolVariation(t *testing.T) { t.Errorf("BoolVariation() error = %v, wanrErr %v", err, test.wantErr) return } + + assert.True(t, errors.Is(err, tt.expectedErr)) + assert.Equal(t, test.want, flag, "%s didn't get expected value", test.name) }) } @@ -420,16 +424,17 @@ func TestCfClient_StringVariation(t *testing.T) { defaultValue string } tests := []struct { - name string - args args - want string - wantErr bool + name string + args args + want string + wantErr bool + expectedErr error }{ - {"Test Invalid Flag Name returns default value", args{"MadeUpIDontExist", target, "foo"}, "foo", false}, - {"Test Default String Flag with when On returns A", args{"TestStringAOn", target, "foo"}, "A", false}, - {"Test Default String Flag when Off returns B", args{"TestStringAOff", target, "foo"}, "B", false}, - {"Test Default String Flag when Pre-Req is False returns B", args{"TestStringAOnWithPreReqFalse", target, "foo"}, "B", false}, - {"Test Default String Flag when Pre-Req is True returns A", args{"TestStringAOnWithPreReqTrue", target, "foo"}, "A", false}, + {"Test Invalid Flag Name returns default value", args{"MadeUpIDontExist", target, "foo"}, "foo", true, DefaultVariationReturnedError}, + {"Test Default String Flag with when On returns A", args{"TestStringAOn", target, "foo"}, "A", false, nil}, + {"Test Default String Flag when Off returns B", args{"TestStringAOff", target, "foo"}, "B", false, nil}, + {"Test Default String Flag when Pre-Req is False returns B", args{"TestStringAOnWithPreReqFalse", target, "foo"}, "B", false, nil}, + {"Test Default String Flag when Pre-Req is True returns A", args{"TestStringAOnWithPreReqTrue", target, "foo"}, "A", false, nil}, } for _, tt := range tests { test := tt @@ -439,15 +444,20 @@ func TestCfClient_StringVariation(t *testing.T) { t.Errorf("BoolVariation() error = %v, wanrErr %v", err, test.wantErr) return } + + assert.True(t, errors.Is(err, tt.expectedErr)) + assert.Equal(t, test.want, flag, "%s didn't get expected value", test.name) }) } } func TestCfClient_DefaultVariationReturned(t *testing.T) { + tests := []struct { name string clientFunc func() (*CfClient, error) + flagIdentifier string mockResponder func() expectedBool bool expectedString string @@ -457,35 +467,37 @@ func TestCfClient_DefaultVariationReturned(t *testing.T) { expectedError error }{ { - name: "Evaluations with Synchronous client with empty SDK key", + name: "Evaluations with Synchronous client with empty SDK flagIdentifier", clientFunc: func() (*CfClient, error) { return newClient(http.DefaultClient, EmptySDKKey, WithWaitForInitialized(true)) }, + flagIdentifier: "made up", expectedBool: false, expectedString: "a default value", expectedInt: 45555, expectedNumber: 45.222, - expectedJSON: types.JSON{"a default key": "a default value"}, + expectedJSON: types.JSON{"a default flagIdentifier": "a default value"}, expectedError: DefaultVariationReturnedError, }, { - name: "Evaluations with Synchronous client with invalid SDK key", + name: "Evaluations with Synchronous client with invalid SDK flagIdentifier", clientFunc: func() (*CfClient, error) { return newClient(http.DefaultClient, InvaliDSDKKey, WithWaitForInitialized(true)) }, mockResponder: func() { bodyString := `{ - "message": "invalid key or target provided", + "message": "invalid flagIdentifier or target provided", "code": "401" }` authErrorResponse := AuthResponseDetailed(401, "401", bodyString) registerResponders(authErrorResponse, TargetSegmentsResponse, FeatureConfigsResponse) }, + flagIdentifier: "made up", expectedBool: false, expectedString: "a default value", expectedInt: 45555, expectedNumber: 45.222, - expectedJSON: types.JSON{"a default key": "a default value"}, + expectedJSON: types.JSON{"a default flagIdentifier": "a default value"}, expectedError: DefaultVariationReturnedError, }, { @@ -501,43 +513,46 @@ func TestCfClient_DefaultVariationReturned(t *testing.T) { authErrorResponse := AuthResponseDetailed(500, "internal server error", bodyString) registerResponders(authErrorResponse, TargetSegmentsResponse, FeatureConfigsResponse) }, + flagIdentifier: "made up", expectedBool: false, expectedString: "a default value", expectedInt: 45555, expectedNumber: 45.222, - expectedJSON: types.JSON{"a default key": "a default value"}, + expectedJSON: types.JSON{"a default flagIdentifier": "a default value"}, expectedError: DefaultVariationReturnedError, }, { - name: "Evaluations with Synchronous client with empty SDK key", + name: "Evaluations with Synchronous client with empty SDK flagIdentifier", clientFunc: func() (*CfClient, error) { return newClient(http.DefaultClient, EmptySDKKey) }, + flagIdentifier: "made up", expectedBool: false, expectedString: "a default value", expectedInt: 45555, expectedNumber: 45.222, - expectedJSON: types.JSON{"a default key": "a default value"}, + expectedJSON: types.JSON{"a default flagIdentifier": "a default value"}, expectedError: DefaultVariationReturnedError, }, { - name: "Evaluations with Async client with invalid SDK key", + name: "Evaluations with Async client with invalid SDK flagIdentifier", clientFunc: func() (*CfClient, error) { return newClient(http.DefaultClient, InvaliDSDKKey) }, mockResponder: func() { bodyString := `{ - "message": "invalid key or target provided", + "message": "invalid flagIdentifier or target provided", "code": "401" }` authErrorResponse := AuthResponseDetailed(401, "401", bodyString) registerResponders(authErrorResponse, TargetSegmentsResponse, FeatureConfigsResponse) }, + flagIdentifier: "made up", expectedBool: false, expectedString: "a default value", expectedInt: 45555, expectedNumber: 45.222, - expectedJSON: types.JSON{"a default key": "a default value"}, + expectedJSON: types.JSON{"a default flagIdentifier": "a default value"}, expectedError: DefaultVariationReturnedError, }, { @@ -553,23 +568,43 @@ func TestCfClient_DefaultVariationReturned(t *testing.T) { authErrorResponse := AuthResponseDetailed(500, "internal server error", bodyString) registerResponders(authErrorResponse, TargetSegmentsResponse, FeatureConfigsResponse) }, + flagIdentifier: "made up", expectedBool: false, expectedString: "a default value", expectedInt: 45555, expectedNumber: 45.222, - expectedJSON: types.JSON{"a default key": "a default value"}, + expectedJSON: types.JSON{"a default flagIdentifier": "a default value"}, expectedError: DefaultVariationReturnedError, }, { - name: "Evaluations with Async client with empty SDK key", + name: "Evaluations with Async client with empty SDK flagIdentifier", clientFunc: func() (*CfClient, error) { return newClient(http.DefaultClient, EmptySDKKey) }, + flagIdentifier: "made up", + expectedBool: false, + expectedString: "a default value", + expectedInt: 45555, + expectedNumber: 45.222, + expectedJSON: types.JSON{"a default flagIdentifier": "a default value"}, + expectedError: DefaultVariationReturnedError, + }, + { + name: "Evaluations with Sync client with valid sdk key and flag not found", + clientFunc: func() (*CfClient, error) { + return newClient(http.DefaultClient, ValidSDKKey, WithWaitForInitialized(true)) + }, + mockResponder: func() { + authSuccessResponse := AuthResponse(200, ValidAuthToken) + registerResponders(authSuccessResponse, TargetSegmentsResponse, FeatureConfigsResponse) + + }, + flagIdentifier: "made up", expectedBool: false, expectedString: "a default value", expectedInt: 45555, expectedNumber: 45.222, - expectedJSON: types.JSON{"a default key": "a default value"}, + expectedJSON: types.JSON{"a default flagIdentifier": "a default value"}, expectedError: DefaultVariationReturnedError, }, } @@ -582,23 +617,23 @@ func TestCfClient_DefaultVariationReturned(t *testing.T) { } client, _ := tt.clientFunc() - boolResult, err := client.BoolVariation("TestTrueOn", target, false) + boolResult, err := client.BoolVariation(tt.flagIdentifier, target, false) assert.Equal(t, tt.expectedBool, boolResult) assert.True(t, errors.Is(err, tt.expectedError)) - stringResult, err := client.StringVariation("TestTrueOn", target, "a default value") + stringResult, err := client.StringVariation(tt.flagIdentifier, target, "a default value") assert.Equal(t, tt.expectedString, stringResult) assert.True(t, errors.Is(err, tt.expectedError)) - intResult, err := client.IntVariation("TestTrueOn", target, tt.expectedInt) + intResult, err := client.IntVariation(tt.flagIdentifier, target, tt.expectedInt) assert.Equal(t, tt.expectedInt, intResult) assert.True(t, errors.Is(err, tt.expectedError)) - numerResult, err := client.NumberVariation("TestTrueOn", target, tt.expectedNumber) + numerResult, err := client.NumberVariation(tt.flagIdentifier, target, tt.expectedNumber) assert.Equal(t, tt.expectedNumber, numerResult) assert.True(t, errors.Is(err, tt.expectedError)) - jsonResult, _ := client.JSONVariation("TestTrueOn", target, tt.expectedJSON) + jsonResult, err := client.JSONVariation(tt.flagIdentifier, target, tt.expectedJSON) assert.Equal(t, tt.expectedJSON, jsonResult) assert.True(t, errors.Is(err, tt.expectedError)) }) diff --git a/client/errors.go b/client/errors.go index b5930637..19bc5d6b 100644 --- a/client/errors.go +++ b/client/errors.go @@ -5,9 +5,10 @@ import ( "fmt" ) -var DefaultVariationReturnedError = errors.New("default variation was returned") - -var EmptySDKKeyError = errors.New("default variation was returned") +var ( + EmptySDKKeyError = errors.New("default variation was returned") + DefaultVariationReturnedError = errors.New("default variation was returned") +) type NonRetryableAuthError struct { StatusCode string diff --git a/evaluation/evaluator.go b/evaluation/evaluator.go index 9acf5c97..bd975154 100644 --- a/evaluation/evaluator.go +++ b/evaluation/evaluator.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/harness/ff-golang-server-sdk/sdk_codes" "regexp" "sort" "strconv" @@ -400,67 +401,63 @@ func (e Evaluator) getVariationForTheFlag(flag *rest.FeatureConfig, target *Targ } // BoolVariation returns boolean evaluation for target -func (e Evaluator) BoolVariation(identifier string, target *Target, defaultValue bool) bool { +func (e Evaluator) BoolVariation(identifier string, target *Target, defaultValue bool) (bool, error) { //flagVariation, err := e.evaluate(identifier, target, "boolean") flagVariation, err := e.evaluate(identifier, target) if err != nil { - e.logger.Errorf("Error while evaluating boolean flag '%s', err: %v", identifier, err) - return defaultValue + return defaultValue, err } - return strings.ToLower(flagVariation.Variation.Value) == "true" + return strings.ToLower(flagVariation.Variation.Value) == "true", nil } // StringVariation returns string evaluation for target -func (e Evaluator) StringVariation(identifier string, target *Target, defaultValue string) string { +func (e Evaluator) StringVariation(identifier string, target *Target, defaultValue string) (string, error) { flagVariation, err := e.evaluate(identifier, target) if err != nil { - e.logger.Errorf("Error while evaluating string flag '%s', err: %v", identifier, err) - return defaultValue + return defaultValue, err } - return flagVariation.Variation.Value + return flagVariation.Variation.Value, nil } // IntVariation returns int evaluation for target -func (e Evaluator) IntVariation(identifier string, target *Target, defaultValue int) int { +func (e Evaluator) IntVariation(identifier string, target *Target, defaultValue int) (int, error) { flagVariation, err := e.evaluate(identifier, target) if err != nil { - e.logger.Errorf("Error while evaluating int flag '%s', err: %v", identifier, err) - return defaultValue + return defaultValue, err } val, err := strconv.Atoi(flagVariation.Variation.Value) if err != nil { - return defaultValue + return defaultValue, err } - return val + return val, nil } // NumberVariation returns number evaluation for target -func (e Evaluator) NumberVariation(identifier string, target *Target, defaultValue float64) float64 { +func (e Evaluator) NumberVariation(identifier string, target *Target, defaultValue float64) (float64, error) { //all numbers are stored as ints in the database flagVariation, err := e.evaluate(identifier, target) if err != nil { - e.logger.Errorf("Error while evaluating number flag '%s', err: %v", identifier, err) - return defaultValue + return defaultValue, err } val, err := strconv.ParseFloat(flagVariation.Variation.Value, 64) if err != nil { - return defaultValue + return defaultValue, err } - return val + return val, nil } // JSONVariation returns json evaluation for target func (e Evaluator) JSONVariation(identifier string, target *Target, - defaultValue map[string]interface{}) map[string]interface{} { + defaultValue map[string]interface{}) (map[string]interface{}, error) { flagVariation, err := e.evaluate(identifier, target) if err != nil { - e.logger.Errorf("Error while evaluating json flag '%s', err: %v", identifier, err) - return defaultValue + return defaultValue, err } val := make(map[string]interface{}) err = json.Unmarshal([]byte(flagVariation.Variation.Value), &val) if err != nil { - return defaultValue + return defaultValue, err } - return val + e.logger.Debugf("%s Evaluated json flag successfully: '%s'", sdk_codes.EvaluationSuccess, identifier) + return val, nil } diff --git a/evaluation/evaluator_test.go b/evaluation/evaluator_test.go index 5ded2151..99accd4a 100644 --- a/evaluation/evaluator_test.go +++ b/evaluation/evaluator_test.go @@ -1420,10 +1420,11 @@ func TestEvaluator_BoolVariation(t *testing.T) { defaultValue bool } tests := []struct { - name string - fields fields - args args - want bool + name string + fields fields + args args + want bool + wantErr bool }{ { name: "bool flag not found return default value", @@ -1435,7 +1436,8 @@ func TestEvaluator_BoolVariation(t *testing.T) { target: nil, defaultValue: false, }, - want: false, + want: false, + wantErr: true, }, { name: "bool evaluation of flag 'simple' should return true", @@ -1470,9 +1472,17 @@ func TestEvaluator_BoolVariation(t *testing.T) { query: tt.fields.query, logger: logger.NewNoOpLogger(), } - if got := e.BoolVariation(tt.args.identifier, tt.args.target, tt.args.defaultValue); got != tt.want { + got, err := e.BoolVariation(tt.args.identifier, tt.args.target, tt.args.defaultValue) + + if (err != nil) != tt.wantErr { + t.Errorf("BoolVariation() error = %v, wanrErr %v", err, tt.wantErr) + return + } + + if got != tt.want { t.Errorf("Evaluator.BoolVariation() = %v, want %v", got, tt.want) } + }) } } @@ -1487,10 +1497,11 @@ func TestEvaluator_StringVariation(t *testing.T) { defaultValue string } tests := []struct { - name string - fields fields - args args - want string + name string + fields fields + args args + want string + wantErr bool }{ { name: "string flag not found return default value", @@ -1502,7 +1513,8 @@ func TestEvaluator_StringVariation(t *testing.T) { target: nil, defaultValue: darktheme, }, - want: darktheme, + want: darktheme, + wantErr: true, }, { name: "string evaluation of flag 'theme' should return lightheme", @@ -1537,7 +1549,7 @@ func TestEvaluator_StringVariation(t *testing.T) { query: tt.fields.query, logger: logger.NewNoOpLogger(), } - if got := e.StringVariation(tt.args.identifier, tt.args.target, tt.args.defaultValue); got != tt.want { + if got, _ := e.StringVariation(tt.args.identifier, tt.args.target, tt.args.defaultValue); got != tt.want { t.Errorf("Evaluator.StringVariation() = %v, want %v", got, tt.want) } }) @@ -1616,7 +1628,7 @@ func TestEvaluator_IntVariation(t *testing.T) { query: tt.fields.query, logger: logger.NewNoOpLogger(), } - if got := e.IntVariation(tt.args.identifier, tt.args.target, tt.args.defaultValue); got != tt.want { + if got, _ := e.IntVariation(tt.args.identifier, tt.args.target, tt.args.defaultValue); got != tt.want { t.Errorf("Evaluator.IntVariation() = %v, want %v", got, tt.want) } }) @@ -1695,7 +1707,7 @@ func TestEvaluator_NumberVariation(t *testing.T) { query: tt.fields.query, logger: logger.NewNoOpLogger(), } - if got := e.NumberVariation(tt.args.identifier, tt.args.target, tt.args.defaultValue); got != tt.want { + if got, _ := e.NumberVariation(tt.args.identifier, tt.args.target, tt.args.defaultValue); got != tt.want { t.Errorf("Evaluator.NumberVariation() = %v, want %v", got, tt.want) } }) @@ -1781,7 +1793,7 @@ func TestEvaluator_JSONVariation(t *testing.T) { query: tt.fields.query, logger: logger.NewNoOpLogger(), } - if got := e.JSONVariation(tt.args.identifier, tt.args.target, tt.args.defaultValue); !reflect.DeepEqual(got, tt.want) { + if got, _ := e.JSONVariation(tt.args.identifier, tt.args.target, tt.args.defaultValue); !reflect.DeepEqual(got, tt.want) { t.Errorf("Evaluator.JSONVariation() = %v, want %v", got, tt.want) } }) diff --git a/sdk_codes/sdk_codes.go b/sdk_codes/sdk_codes.go new file mode 100644 index 00000000..4958374b --- /dev/null +++ b/sdk_codes/sdk_codes.go @@ -0,0 +1,31 @@ +package sdk_codes + +// SDKCode is the type for codes that are logged for each lifecycle of the SDK +type SDKCode string + +const ( + InitSuccess SDKCode = "SDKCODE:1000" + InitAuthError SDKCode = "SDKCODE:1001" + InitMissingKey SDKCode = "SDKCODE:1002" + InitWaiting SDKCode = "SDKCODE:1003" + AuthSuccess SDKCode = "SDKCODE:2000" + AuthFailed SDKCode = "SDKCODE:2001" + AuthAttempt SDKCode = "SDKCODE:2002" + AuthExceededRetries SDKCode = "SDKCODE:2003" + CloseStarted SDKCode = "SDKCODE:3000" + CloseSuccess SDKCode = "SDKCODE:3001" + PollStart SDKCode = "SDKCODE:4000" + PollStop SDKCode = "SDKCODE:4001" + StreamStarted SDKCode = "SDKCODE:5000" + StreamDisconnected SDKCode = "SDKCODE:5001" + StreamEvent SDKCode = "SDKCODE:5002" + // StreamRetry TODO it's not clear how the SSE retry mechanism is working. Add this once SSE resilency has been established in FFM-9485 + StreamRetry SDKCode = "SDKCODE:5003" + StreamStop SDKCode = "SDKCODE:5004" + EvaluationSuccess SDKCode = "SDKCODE:6000" + EvaluationFailed SDKCode = "SDKCODE:6001" + MetricsStarted SDKCode = "SDKCODE:7000" + MetricsStopped SDKCode = "SDKCODE:7001" + MetricsSendFail SDKCode = "SDKCODE:7002" + MetricsSendSuccess SDKCode = "SDKCODE:7003" +) diff --git a/stream/sse.go b/stream/sse.go index 46dd9b49..e860d62f 100644 --- a/stream/sse.go +++ b/stream/sse.go @@ -3,6 +3,7 @@ package stream import ( "context" "fmt" + "github.com/harness/ff-golang-server-sdk/sdk_codes" "time" "github.com/harness/ff-golang-server-sdk/pkg/repository" @@ -71,7 +72,7 @@ func (c *SSEClient) Connect(ctx context.Context, environment string, apiKey stri // Connect will subscribe to SSE stream func (c *SSEClient) subscribe(ctx context.Context, environment string, apiKey string) <-chan Event { - c.logger.Infof("Start subscribing to Stream") + c.logger.Infof("%s Start subscribing to Stream", sdk_codes.StreamStarted) // don't use the default exponentialBackoff strategy - we have our own disconnect logic // of polling the service then re-establishing a new stream once we can connect c.client.ReconnectStrategy = &backoff.StopBackOff{} @@ -82,7 +83,7 @@ func (c *SSEClient) subscribe(ctx context.Context, environment string, apiKey st defer close(out) err := c.client.SubscribeWithContext(ctx, "*", func(msg *sse.Event) { - c.logger.Infof("Event received: %s", msg.Data) + c.logger.Infof("%s Event received: %s", sdk_codes.StreamEvent, msg.Data) if len(msg.Data) <= 0 { return diff --git a/tests/evaluator_test.go b/tests/evaluator_test.go index cd6c69f0..0c47f24a 100644 --- a/tests/evaluator_test.go +++ b/tests/evaluator_test.go @@ -121,13 +121,13 @@ func TestEvaluator(t *testing.T) { } switch flag.Kind { case rest.FeatureConfigKindBoolean: - got = evaluator.BoolVariation(testCase.Flag, target, false) + got, _ = evaluator.BoolVariation(testCase.Flag, target, false) case rest.FeatureConfigKindString: - got = evaluator.StringVariation(testCase.Flag, target, "blue") + got, _ = evaluator.StringVariation(testCase.Flag, target, "blue") case rest.FeatureConfigKindInt, "number": - got = evaluator.NumberVariation(testCase.Flag, target, 50.00) + got, _ = evaluator.NumberVariation(testCase.Flag, target, 50.00) case rest.FeatureConfigKindJson: - got = evaluator.JSONVariation(testCase.Flag, target, map[string]interface{}{}) + got, _ = evaluator.JSONVariation(testCase.Flag, target, map[string]interface{}{}) str, _ := json.Marshal(&got) got = string(str)