diff --git a/localEvaluation/localEvaluation.go b/localEvaluation/localEvaluation.go index 170df92..8785773 100644 --- a/localEvaluation/localEvaluation.go +++ b/localEvaluation/localEvaluation.go @@ -30,6 +30,7 @@ type variant struct { type UserProperties struct { OrgId string `json:"org_id,omitempty"` + UserId string `json:"user_id,omitempty"` OrgName string `json:"org_name,omitempty"` Username string `json:"username,omitempty"` UserStatus string `json:"user_status,omitempty"` @@ -82,7 +83,7 @@ func Initialize() { } } -func fetch(flagName string, user UserProperties) variant { +func fetch(flagName string, user UserProperties) map[string]experiment.Variant { flagKeys := []string{flagName} userProp := map[string]interface{}{ "org_id": user.OrgId, @@ -97,25 +98,25 @@ func fetch(flagName string, user UserProperties) variant { } expUser := experiment.User{ + UserId: user.UserId, UserProperties: userProp, } variants, err := client.Evaluate(&expUser, flagKeys) if err != nil { - return variant{} + return map[string]experiment.Variant{} } - - return variant(variants[flagName]) + return variants } func GetFeatureFlagString(flagName string, user UserProperties) string { data := fetch(flagName, user) - return data.Value + return data[flagName].Value } func GetFeatureFlagBool(flagName string, user UserProperties) bool { data := fetch(flagName, user) - if val, err := strconv.ParseBool(data.Value); err == nil { + if val, err := strconv.ParseBool(data[flagName].Value); err == nil { return val } return false @@ -124,7 +125,7 @@ func GetFeatureFlagBool(flagName string, user UserProperties) bool { func GetFeatureFlagPayload(flagName string, user UserProperties) map[string]interface{} { data := fetch(flagName, user) mapData := make(map[string]interface{}) - mapData["value"] = data.Value - mapData["payload"] = data.Payload + mapData["value"] = data[flagName].Value + mapData["payload"] = data[flagName].Payload return mapData } diff --git a/pkg/experiment/local/client.go b/pkg/experiment/local/client.go index 367319e..1318f95 100644 --- a/pkg/experiment/local/client.go +++ b/pkg/experiment/local/client.go @@ -1,16 +1,18 @@ package local import ( + "bytes" "context" "encoding/json" "fmt" + "github.com/LambdaTest/lambda-featureflag-go-sdk/internal/evaluation" + "io" "io/ioutil" "net/http" "net/url" + "os" "sync" - "github.com/LambdaTest/lambda-featureflag-go-sdk/internal/evaluation" - "github.com/LambdaTest/lambda-featureflag-go-sdk/pkg/experiment" "github.com/LambdaTest/lambda-featureflag-go-sdk/internal/logger" @@ -19,6 +21,8 @@ import ( var clients = map[string]*Client{} var initMutex = sync.Mutex{} +const EXPOSURE_EVENT_TYPE = "$exposure" + type Client struct { log *logger.Log apiKey string @@ -28,6 +32,20 @@ type Client struct { flags *string } +type Event struct { + EventType string `json:"event_type"` + UserId string `json:"user_id"` + EventProperties struct { + FlagKey string `json:"flag_key"` + Variant interface{} `json:"variant"` + } `json:"event_properties"` +} + +type ExposurePayload struct { + ApiKey string `json:"api_key"` + Events []Event `json:"events"` +} + func Initialize(apiKey string, config *Config) *Client { initMutex.Lock() client := clients[apiKey] @@ -82,6 +100,7 @@ func (c *Client) Evaluate(user *experiment.User, flagKeys []string) (map[string] resultJson := evaluation.Evaluate(*c.flags, string(userJson)) c.log.Debug("evaluate result: %v\n", resultJson) + go c.exposure(resultJson, user.UserId) var interopResult *interopResult err = json.Unmarshal([]byte(resultJson), &interopResult) if err != nil { @@ -186,3 +205,64 @@ func contains(s []string, e string) bool { } return false } + +func (c *Client) exposure(resultJson string, userId string) { + parsePayload := map[string]interface{}{} + err := json.Unmarshal([]byte(resultJson), &parsePayload) + if err != nil { + c.log.Error("unable to parse string %s with error %s", resultJson, err.Error()) + return + } + payload := ExposurePayload{} + payload.ApiKey = os.Getenv("ANALYTICS_API_KEY") + if result, ok := parsePayload["result"].(map[string]interface{}); ok { + for flagKey, flagValue := range result { + event := Event{} + event.EventType = EXPOSURE_EVENT_TYPE + event.UserId = userId + event.EventProperties.FlagKey = flagKey + if flagResult, ok := flagValue.(map[string]interface{}); ok { + if variant, ok := flagResult["variant"].(map[string]interface{}); ok { + event.EventProperties.Variant = variant + } + } + payload.Events = append(payload.Events, event) + } + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + c.log.Error("unable to marsal payload %+v with error %s", payload, err.Error()) + return + } + c.log.Debug("exposure payload : %s", string(payloadBytes)) + + ctx, cancel := context.WithTimeout(context.Background(), c.config.FlagConfigPollerRequestTimeout) + defer cancel() + req, err := http.NewRequest(http.MethodPost, "https://api2.amplitude.com/2/httpapi", bytes.NewBuffer(payloadBytes)) + if err != nil { + c.log.Error("unable to create request with error %s", err.Error()) + return + } + req = req.WithContext(ctx) + req.Header.Set("Content-Type", "application/json; charset=UTF-8") + var client = &http.Client{ + Timeout: c.config.FlagConfigPollerRequestTimeout, + Transport: &http.Transport{ + MaxIdleConns: 5, + MaxIdleConnsPerHost: 5, + DisableKeepAlives: true, + }, + } + resp, err := client.Do(req) + if err != nil { + c.log.Error("error %s in making call to amplitude server", err.Error()) + return + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + c.log.Error("error %s while reading response", err.Error()) + return + } + c.log.Debug("exposure response: %s", string(body)) +} diff --git a/pkg/experiment/remote/client.go b/pkg/experiment/remote/client.go deleted file mode 100644 index c801e4d..0000000 --- a/pkg/experiment/remote/client.go +++ /dev/null @@ -1,167 +0,0 @@ -package remote - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "math" - "math/rand" - "net/http" - "net/url" - "sync" - "time" - - "github.com/LambdaTest/lambda-featureflag-go-sdk/pkg/experiment" - - "github.com/LambdaTest/lambda-featureflag-go-sdk/internal/logger" -) - -var clients = map[string]*Client{} -var initMutex = sync.Mutex{} - -type Client struct { - log *logger.Log - apiKey string - config *Config - client *http.Client -} - -func Initialize(apiKey string, config *Config) *Client { - initMutex.Lock() - client := clients[apiKey] - if client == nil { - if apiKey == "" { - panic("api key must be set") - } - config = fillConfigDefaults(config) - client = &Client{ - log: logger.New(config.Debug), - apiKey: apiKey, - config: config, - client: &http.Client{}, - } - client.log.Debug("config: %v", *config) - } - initMutex.Unlock() - return client -} - -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 { - return c.retryFetch(user) - } else { - return nil, err - } - } - return variants, err -} - -func (c *Client) doFetch(user *experiment.User, timeout time.Duration) (map[string]experiment.Variant, error) { - addLibraryContext(user) - endpoint, err := url.Parse(c.config.ServerUrl) - if err != nil { - return nil, err - } - endpoint.Path = "sdk/vardata" - if c.config.Debug { - endpoint.RawQuery = fmt.Sprintf("d=%s", randStringRunes(5)) - } - jsonBytes, err := json.Marshal(user) - if err != nil { - return nil, err - } - 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)) - 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") - c.log.Debug("fetch request: %v", req) - resp, err := c.client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - c.log.Debug("fetch response: %v", *resp) - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("fetch request resulted in error response %v", resp.StatusCode) - } - return c.parseResponse(resp) -} - -func (c *Client) retryFetch(user *experiment.User) (map[string]experiment.Variant, error) { - var err error - var variants map[string]experiment.Variant - var timer *time.Timer - delay := c.config.RetryBackoff.FetchRetryBackoffMin - for i := 0; i < c.config.RetryBackoff.FetchRetries; i++ { - c.log.Debug("retry attempt %v", i) - timer = time.NewTimer(delay) - <-timer.C - variants, err = c.doFetch(user, c.config.RetryBackoff.FetchRetryTimeout) - if err == nil && variants != nil { - c.log.Debug("retry attempt %v success", i) - return variants, nil - } - c.log.Debug("retry attempt %v error: %v", i, err) - delay = time.Duration(math.Min( - float64(delay)*c.config.RetryBackoff.FetchRetryBackoffScalar, - float64(c.config.RetryBackoff.FetchRetryBackoffMax)), - ) - } - c.log.Error("fetch retries failed after %v attempts: %v", c.config.RetryBackoff.FetchRetries, err) - return nil, err -} - -func (c *Client) parseResponse(resp *http.Response) (map[string]experiment.Variant, error) { - interop := make(interopVariants) - err := json.NewDecoder(resp.Body).Decode(&interop) - 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 -} - -func addLibraryContext(user *experiment.User) { - if user.Library == "" { - user.Library = fmt.Sprintf("experiment-go-server/%v", experiment.VERSION) - } -} - -// Helper - -func init() { - rand.Seed(time.Now().UnixNano()) -} - -var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - -func randStringRunes(n int) string { - b := make([]rune, n) - for i := range b { - b[i] = letterRunes[rand.Intn(len(letterRunes))] - } - return string(b) -} diff --git a/pkg/experiment/remote/config.go b/pkg/experiment/remote/config.go deleted file mode 100644 index aedd236..0000000 --- a/pkg/experiment/remote/config.go +++ /dev/null @@ -1,49 +0,0 @@ -package remote - -import "time" - -type Config struct { - Debug bool - ServerUrl string - FetchTimeout time.Duration - RetryBackoff *RetryBackoff -} - -var DefaultConfig = &Config{ - Debug: false, - ServerUrl: "https://api.lab.amplitude.com/", - FetchTimeout: 500 * time.Millisecond, - RetryBackoff: DefaultRetryBackoff, -} - -type RetryBackoff struct { - FetchRetries int - FetchRetryBackoffMin time.Duration - FetchRetryBackoffMax time.Duration - FetchRetryBackoffScalar float64 - FetchRetryTimeout time.Duration -} - -var DefaultRetryBackoff = &RetryBackoff{ - FetchRetries: 1, - FetchRetryBackoffMin: 0 * time.Millisecond, - FetchRetryBackoffMax: 10000 * time.Millisecond, - FetchRetryBackoffScalar: 1, - FetchRetryTimeout: 500 * time.Millisecond, -} - -func fillConfigDefaults(c *Config) *Config { - if c == nil { - return DefaultConfig - } - if c.ServerUrl == "" { - c.ServerUrl = DefaultConfig.ServerUrl - } - if c.FetchTimeout == 0 { - c.FetchTimeout = DefaultConfig.FetchTimeout - } - if c.RetryBackoff == nil { - c.RetryBackoff = DefaultConfig.RetryBackoff - } - return c -} 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