From c874a24858ed834a1fc3fc03cdca1caa65a12e4d Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Thu, 18 Apr 2024 16:36:59 -0700 Subject: [PATCH 1/3] feat: fetch v2 for remote eval client --- pkg/experiment/remote/client.go | 60 +++++++++++++++++++--------- pkg/experiment/remote/client_test.go | 22 ++++++++++ pkg/experiment/remote/types.go | 9 ----- 3 files changed, 63 insertions(+), 28 deletions(-) delete mode 100644 pkg/experiment/remote/types.go diff --git a/pkg/experiment/remote/client.go b/pkg/experiment/remote/client.go index 5ad832f..4c35718 100644 --- a/pkg/experiment/remote/client.go +++ b/pkg/experiment/remote/client.go @@ -1,8 +1,8 @@ package remote import ( - "bytes" "context" + "encoding/base64" "encoding/json" "fmt" "math" @@ -47,7 +47,24 @@ func Initialize(apiKey string, config *Config) *Client { return client } +// Deprecated: Use FetchV2 func (c *Client) Fetch(user *experiment.User) (map[string]experiment.Variant, error) { + variants, err := c.doFetch(user, c.config.FetchTimeout) + if err != nil { + c.log.Error("fetch error: %v", err) + if c.config.RetryBackoff.FetchRetries > 0 && shouldRetryFetch(err) { + return c.retryFetch(user) + } else { + return nil, err + } + } + results := filterDefaultVariants(variants) + return results, nil +} + +// FetchV2 fetches variants for a user from the remote evaluation service. +// Unlike Fetch, this method returns all variants, including default variants. +func (c *Client) FetchV2(user *experiment.User) (map[string]experiment.Variant, error) { variants, err := c.doFetch(user, c.config.FetchTimeout) if err != nil { c.log.Error("fetch error: %v", err) @@ -66,7 +83,7 @@ func (c *Client) doFetch(user *experiment.User, timeout time.Duration) (map[stri if err != nil { return nil, err } - endpoint.Path = "sdk/vardata" + endpoint.Path = "sdk/v2/vardata" if c.config.Debug { endpoint.RawQuery = fmt.Sprintf("d=%s", randStringRunes(5)) } @@ -77,13 +94,13 @@ func (c *Client) doFetch(user *experiment.User, timeout time.Duration) (map[stri c.log.Debug("fetch variants for user %s", string(jsonBytes)) ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - req, err := http.NewRequest("POST", endpoint.String(), bytes.NewBuffer(jsonBytes)) + req, err := http.NewRequestWithContext(ctx, "GET", endpoint.String(), nil) if err != nil { return nil, err } - req = req.WithContext(ctx) req.Header.Set("Authorization", fmt.Sprintf("Api-Key %s", c.apiKey)) req.Header.Set("Content-Type", "application/json; charset=UTF-8") + req.Header.Set("X-Amp-Exp-User", base64.StdEncoding.EncodeToString(jsonBytes)) c.log.Debug("fetch request: %v", req) resp, err := c.client.Do(req) if err != nil { @@ -122,24 +139,11 @@ func (c *Client) retryFetch(user *experiment.User) (map[string]experiment.Varian } func (c *Client) parseResponse(resp *http.Response) (map[string]experiment.Variant, error) { - interop := make(interopVariants) - err := json.NewDecoder(resp.Body).Decode(&interop) + variants := make(map[string]experiment.Variant) + err := json.NewDecoder(resp.Body).Decode(&variants) if err != nil { return nil, err } - variants := make(map[string]experiment.Variant) - for k, iv := range interop { - var value string - if iv.Value != "" { - value = iv.Value - } else if iv.Key != "" { - value = iv.Key - } - variants[k] = experiment.Variant{ - Value: value, - Payload: iv.Payload, - } - } c.log.Debug("parsed variants from response: %v", variants) return variants, nil } @@ -172,3 +176,21 @@ func shouldRetryFetch(err error) bool { } return true } + +func filterDefaultVariants(variants map[string]experiment.Variant) map[string]experiment.Variant { + results := make(map[string]experiment.Variant) + for key, variant := range variants { + isDefault, ok := variant.Metadata["default"].(bool) + if !ok { + isDefault = false + } + isDeployed, ok := variant.Metadata["deployed"].(bool) + if !ok { + isDeployed = true + } + if !isDefault && isDeployed { + results[key] = variant + } + } + return results +} diff --git a/pkg/experiment/remote/client_test.go b/pkg/experiment/remote/client_test.go index 5e0b625..59d9d45 100644 --- a/pkg/experiment/remote/client_test.go +++ b/pkg/experiment/remote/client_test.go @@ -12,6 +12,28 @@ import ( "github.com/stretchr/testify/require" ) +func TestClient_Fetch_DoesNotReturnDefaultVariants(t *testing.T) { + client := Initialize("server-qz35UwzJ5akieoAdIgzM4m9MIiOLXLoz", nil) + user := &experiment.User{} + result, err := client.Fetch(user) + require.NoError(t, err) + require.NotNil(t, result) + variant := result["sdk-ci-test"] + require.Empty(t, variant) +} + + +func TestClient_FetchV2_ReturnsDefaultVariants(t *testing.T) { + client := Initialize("server-qz35UwzJ5akieoAdIgzM4m9MIiOLXLoz", nil) + user := &experiment.User{} + result, err := client.FetchV2(user) + require.NoError(t, err) + require.NotNil(t, result) + variant := result["sdk-ci-test"] + require.NotNil(t, variant) + require.Equal(t, "off", variant.Key) +} + func TestClient_FetchRetryWithDifferentResponseCodes(t *testing.T) { // Test data: Response code, error message, and expected number of fetch calls testData := []struct { diff --git a/pkg/experiment/remote/types.go b/pkg/experiment/remote/types.go deleted file mode 100644 index e823503..0000000 --- a/pkg/experiment/remote/types.go +++ /dev/null @@ -1,9 +0,0 @@ -package remote - -type interopVariant struct { - Value string `json:"value,omitempty"` - Key string `json:"key,omitempty"` - Payload interface{} `json:"payload,omitempty"` -} - -type interopVariants = map[string]interopVariant From 6a70d376d6a5fac4e53f036b3632331ae09bc4e2 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Thu, 18 Apr 2024 16:38:08 -0700 Subject: [PATCH 2/3] simplify --- pkg/experiment/remote/client.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pkg/experiment/remote/client.go b/pkg/experiment/remote/client.go index 4c35718..456a70b 100644 --- a/pkg/experiment/remote/client.go +++ b/pkg/experiment/remote/client.go @@ -49,14 +49,9 @@ func Initialize(apiKey string, config *Config) *Client { // Deprecated: Use FetchV2 func (c *Client) Fetch(user *experiment.User) (map[string]experiment.Variant, error) { - variants, err := c.doFetch(user, c.config.FetchTimeout) + variants, err := c.FetchV2(user) if err != nil { - c.log.Error("fetch error: %v", err) - if c.config.RetryBackoff.FetchRetries > 0 && shouldRetryFetch(err) { - return c.retryFetch(user) - } else { - return nil, err - } + return nil, err } results := filterDefaultVariants(variants) return results, nil From c77bf23878c95f2df44b96421a626db84ce90df1 Mon Sep 17 00:00:00 2001 From: Brian Giori Date: Fri, 19 Apr 2024 08:21:51 -0700 Subject: [PATCH 3/3] use v2 fetch in cmd --- cmd/xpmt/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/xpmt/main.go b/cmd/xpmt/main.go index 9d3d656..832ba69 100644 --- a/cmd/xpmt/main.go +++ b/cmd/xpmt/main.go @@ -90,7 +90,7 @@ func fetch() { client := remote.Initialize(*apiKey, config) start := time.Now() - variants, err := client.Fetch(user) + variants, err := client.FetchV2(user) if err != nil { fmt.Printf("error: %v\n", err) os.Exit(1)