diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..244038b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +vendor/ +.idea/ +.vscode/ +.DS_Store +internal/.DS_Store +local_evaluation.go \ No newline at end of file diff --git a/go.mod b/go.mod index 79eb15c..6513118 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,14 @@ module github.com/LambdaTest/lambda-featureflag-go-sdk go 1.20 require ( + github.com/amplitude/analytics-go v1.0.1 github.com/joho/godotenv v1.5.1 github.com/sirupsen/logrus v1.9.3 + github.com/spaolacci/murmur3 v1.1.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) require ( - github.com/amplitude/analytics-go v1.0.1 // indirect github.com/google/uuid v1.3.0 // indirect - github.com/spaolacci/murmur3 v1.1.0 // indirect golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect ) diff --git a/go.sum b/go.sum index 8bab424..c7c8117 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/amplitude/analytics-go v1.0.1 h1:rrdC5VBctlJigSk0kw7ktwSijob/wyH4bop2SqWduCU= github.com/amplitude/analytics-go v1.0.1/go.mod h1:kAQG8OQ6aPOxZrEZ3+/NFCfxdYSyjqXZhgkjWFD3/vo= -github.com/amplitude/experiment-go-server v1.3.0 h1:zsrbMiaI6yDwNesDREX6Wy83w4kZ0vqJEdxko69DmU8= -github.com/amplitude/experiment-go-server v1.3.0/go.mod h1:5HERnGGohucx2mNr/0fLeQ9xFWNmeMbtzsxH2beB+hs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -16,14 +14,9 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -31,4 +24,3 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/localEvaluation/localEvaluation.go b/localEvaluation/localEvaluation.go index f2e38a2..7e7f124 100644 --- a/localEvaluation/localEvaluation.go +++ b/localEvaluation/localEvaluation.go @@ -2,23 +2,25 @@ package localEvaluation import ( "fmt" - - "github.com/LambdaTest/lambda-featureflag-go-sdk/pkg/experiment" - "github.com/LambdaTest/lambda-featureflag-go-sdk/pkg/experiment/local" - "github.com/joho/godotenv" - "os" "strconv" "time" + + "github.com/joho/godotenv" + + "github.com/LambdaTest/lambda-featureflag-go-sdk/pkg/experiment" + "github.com/LambdaTest/lambda-featureflag-go-sdk/pkg/experiment/local" + "github.com/LambdaTest/lambda-featureflag-go-sdk/pkg/rootOrg" ) var ( client *local.Client - LocalEvaluationConfigDebug = true - LocalEvaluationConfigServerUrl = "https://api.lambdatest.com" - LocalEvaluationConfigPollInterval = 120 - LocalEvaluationConfigPollerRequestTimeout = 60 - LocalEvaluationDeploymentKey = "server-jAqqJaX3l8PgNiJpcv9j20ywPzANQQFh" + rootOrgClient *rootOrg.Client + localEvaluationConfigDebug = true + localEvaluationConfigServerUrl = "https://api.lambdatest.com" + localEvaluationConfigPollInterval = 120 + localEvaluationConfigPollerRequestTimeout = 60 + localEvaluationDeploymentKey = "server-jAqqJaX3l8PgNiJpcv9j20ywPzANQQFh" retries = 5 ) @@ -43,7 +45,7 @@ type UserProperties struct { TemplateId string `json:"template_id,omitempty"` } -func Init() { +func initVars() { err := godotenv.Load() if err != nil { fmt.Printf("No .env file found") @@ -52,31 +54,31 @@ func Init() { } if os.Getenv("LOCAL_EVALUATION_CONFIG_DEBUG") != "" { - LocalEvaluationConfigDebug, _ = strconv.ParseBool(os.Getenv("LOCAL_EVALUATION_CONFIG_DEBUG")) + localEvaluationConfigDebug, _ = strconv.ParseBool(os.Getenv("LOCAL_EVALUATION_CONFIG_DEBUG")) } if os.Getenv("LOCAL_EVALUATION_CONFIG_SERVER_URL") != "" { - LocalEvaluationConfigServerUrl = os.Getenv("LOCAL_EVALUATION_CONFIG_SERVER_URL") + localEvaluationConfigServerUrl = os.Getenv("LOCAL_EVALUATION_CONFIG_SERVER_URL") } if os.Getenv("LOCAL_EVALUATION_CONFIG_POLL_INTERVAL") != "" { - LocalEvaluationConfigPollInterval, _ = strconv.Atoi(os.Getenv("LOCAL_EVALUATION_CONFIG_POLL_INTERVAL")) + localEvaluationConfigPollInterval, _ = strconv.Atoi(os.Getenv("LOCAL_EVALUATION_CONFIG_POLL_INTERVAL")) } if os.Getenv("LOCAL_EVALUATION_CONFIG_POLLER_REQUEST_TIMEOUT") != "" { - LocalEvaluationConfigPollerRequestTimeout, _ = strconv.Atoi(os.Getenv("LOCAL_EVALUATION_CONFIG_POLLER_REQUEST_TIMEOUT")) + localEvaluationConfigPollerRequestTimeout, _ = strconv.Atoi(os.Getenv("LOCAL_EVALUATION_CONFIG_POLLER_REQUEST_TIMEOUT")) } if os.Getenv("LOCAL_EVALUATION_DEPLOYMENT_KEY") != "" { - LocalEvaluationDeploymentKey = os.Getenv("LOCAL_EVALUATION_DEPLOYMENT_KEY") + localEvaluationDeploymentKey = os.Getenv("LOCAL_EVALUATION_DEPLOYMENT_KEY") } } func Initialize() { - Init() + initVars() config := local.Config{ - Debug: LocalEvaluationConfigDebug, - ServerUrl: LocalEvaluationConfigServerUrl, - FlagConfigPollerInterval: time.Duration(LocalEvaluationConfigPollInterval) * time.Second, - FlagConfigPollerRequestTimeout: time.Duration(LocalEvaluationConfigPollerRequestTimeout) * time.Second, + Debug: localEvaluationConfigDebug, + ServerUrl: localEvaluationConfigServerUrl, + FlagConfigPollerInterval: time.Duration(localEvaluationConfigPollInterval) * time.Second, + FlagConfigPollerRequestTimeout: time.Duration(localEvaluationConfigPollerRequestTimeout) * time.Second, } - client = local.Initialize(LocalEvaluationDeploymentKey, &config) + client = local.Initialize(localEvaluationDeploymentKey, &config) var err error for i := 0; i < retries; i++ { err = client.Start() @@ -111,8 +113,32 @@ func InitializeWithConfig(conf local.Config, deploymentKey string) { } } +func InitializeRootOrg() error { + initVars() + rootOrgClient = rootOrg.NewClient(localEvaluationDeploymentKey, &rootOrg.Config{ + ServerUrl: localEvaluationConfigServerUrl, + FlagConfigPollerInterval: time.Duration(localEvaluationConfigPollInterval) * time.Second, + FlagConfigPollerRequestTimeout: time.Duration(localEvaluationConfigPollerRequestTimeout) * time.Second, + }) + var err error + for i := 0; i < retries; i++ { + err = rootOrgClient.Start() + if err != nil { + err = fmt.Errorf("unable to get root orgs with given config %+v attempt:%v with error %s", rootOrgClient.Config, i+1, err.Error()) + continue + } else { + break + } + } + if err != nil { + err = fmt.Errorf("unable to get root orgs with given config %+v with error %s", rootOrgClient.Config, err.Error()) + return err + } + return nil +} + func fetch(user UserProperties) (map[string]experiment.Variant, error) { - userProp := map[string]interface{}{ + userProp := map[string]any{ "org_id": user.OrgId, "org_name": user.OrgName, "username": user.Username, @@ -128,6 +154,14 @@ func fetch(user UserProperties) (map[string]experiment.Variant, error) { UserProperties: userProp, } + // Evaluate root org to get the parent org id + if expUser.UserProperties["org_id"] != "" && rootOrgClient != nil { + rootOrg, ok := rootOrgClient.Evaluate(expUser.UserProperties["org_id"]) + if ok { + expUser.UserProperties["org_id"] = rootOrg + } + } + result, err := client.EvaluateV2(&expUser, []string{}) if err != nil { return nil, err @@ -137,7 +171,7 @@ func fetch(user UserProperties) (map[string]experiment.Variant, error) { func getValue(flagName string, user UserProperties) Variant { result, _ := fetch(user) - if result != nil && len(result) != 0 { + if len(result) != 0 { if value, ok := result[flagName]; ok { return Variant{ Key: value.Key, @@ -151,7 +185,7 @@ func getValue(flagName string, user UserProperties) Variant { func getMapOfValue(user UserProperties) map[string]interface{} { flags := make(map[string]interface{}) result, _ := fetch(user) - if result != nil && len(result) != 0 { + if len(result) != 0 { for k, v := range result { if v.Value != "" { flags[k] = v.Value diff --git a/model/lums.go b/model/lums.go new file mode 100644 index 0000000..74728b8 --- /dev/null +++ b/model/lums.go @@ -0,0 +1,18 @@ +package model + +type OrgId int64 + +type OrgMap map[OrgId]OrgId + +// ErrorResponse represents the error response from the LUMS API +type ErrorResponse struct { + Type string `json:"type"` + Title string `json:"title"` + Message string `json:"message"` +} + +// SuccessResponse represents the success response from the LUMS API +type SuccessResponse struct { + Type string `json:"type"` + Data OrgMap `json:"data"` +} diff --git a/pkg/experiment/local/client.go b/pkg/experiment/local/client.go index e6aabdb..ed0bd16 100644 --- a/pkg/experiment/local/client.go +++ b/pkg/experiment/local/client.go @@ -4,18 +4,17 @@ import ( "context" "encoding/json" "fmt" - "github.com/amplitude/analytics-go/amplitude" "io/ioutil" "net/http" "net/url" "reflect" "sync" - "github.com/LambdaTest/lambda-featureflag-go-sdk/internal/evaluation" - - "github.com/LambdaTest/lambda-featureflag-go-sdk/pkg/experiment" + "github.com/amplitude/analytics-go/amplitude" + "github.com/LambdaTest/lambda-featureflag-go-sdk/internal/evaluation" "github.com/LambdaTest/lambda-featureflag-go-sdk/internal/logger" + "github.com/LambdaTest/lambda-featureflag-go-sdk/pkg/experiment" ) var clients = map[string]*Client{} diff --git a/pkg/rootOrg/client.go b/pkg/rootOrg/client.go new file mode 100644 index 0000000..493a262 --- /dev/null +++ b/pkg/rootOrg/client.go @@ -0,0 +1,65 @@ +package rootOrg + +import ( + "strconv" + "sync" + + "github.com/LambdaTest/lambda-featureflag-go-sdk/model" +) + +type Client struct { + apiKey string + Config *Config + poller *poller + flags *model.OrgMap + flagsMutex *sync.RWMutex +} + +func NewClient(apiKey string, config *Config) *Client { + return &Client{ + apiKey: apiKey, + Config: config, + flagsMutex: &sync.RWMutex{}, + flags: &model.OrgMap{}, + poller: newPoller(), + } +} + +func (c *Client) Start() error { + c.poller = newPoller() + c.poller.Poll(c.Config.FlagConfigPollerInterval, func() { + c.pollFlags() + }) + return c.pollFlags() +} + +func (c *Client) Stop() { + close(c.poller.shutdown) +} + +func (c *Client) pollFlags() error { + c.flagsMutex.Lock() + defer c.flagsMutex.Unlock() + rootOrgs, err := GetRootOrgs(c.apiKey, c.Config.ServerUrl, c.Config.FlagConfigPollerRequestTimeout) + if err != nil { + return err + } + c.flags = rootOrgs + return nil +} + +func (c *Client) Evaluate(orgId any) (any, bool) { + c.flagsMutex.RLock() + defer c.flagsMutex.RUnlock() + + orgIdStr, ok := orgId.(string) + if !ok { + return nil, false + } + orgIdInt, err := strconv.ParseInt(orgIdStr, 10, 64) + if err != nil { + return nil, false + } + org, ok := (*c.flags)[model.OrgId(orgIdInt)] + return org, ok +} diff --git a/pkg/rootOrg/config.go b/pkg/rootOrg/config.go new file mode 100644 index 0000000..0f33383 --- /dev/null +++ b/pkg/rootOrg/config.go @@ -0,0 +1,17 @@ +package rootOrg + +import ( + "time" +) + +type Config struct { + ServerUrl string + FlagConfigPollerInterval time.Duration + FlagConfigPollerRequestTimeout time.Duration +} + +var DefaultConfig = &Config{ + ServerUrl: "https://api.lambdatest.com", + FlagConfigPollerInterval: 120 * time.Second, + FlagConfigPollerRequestTimeout: 60 * time.Second, +} diff --git a/pkg/rootOrg/poller.go b/pkg/rootOrg/poller.go new file mode 100644 index 0000000..0a94863 --- /dev/null +++ b/pkg/rootOrg/poller.go @@ -0,0 +1,28 @@ +package rootOrg + +import "time" + +type poller struct { + shutdown chan struct{} +} + +func newPoller() *poller { + return &poller{ + shutdown: make(chan struct{}), + } +} + +func (p *poller) Poll(interval time.Duration, function func()) { + ticker := time.NewTicker(interval) + go func() { + for { + select { + case <-p.shutdown: + ticker.Stop() + return + case <-ticker.C: + go function() + } + } + }() +} diff --git a/pkg/rootOrg/rootOrg.go b/pkg/rootOrg/rootOrg.go new file mode 100644 index 0000000..6f75819 --- /dev/null +++ b/pkg/rootOrg/rootOrg.go @@ -0,0 +1,70 @@ +package rootOrg + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/LambdaTest/lambda-featureflag-go-sdk/model" +) + +// lumsClient handles LUMS API operations +type lumsClient struct { + httpClient *http.Client + baseURL string + timeout time.Duration +} + +// NewLumsClient creates a new LUMSClient +func newLumsClient(httpClient *http.Client, url string, timeout time.Duration) *lumsClient { + return &lumsClient{ + httpClient: httpClient, + baseURL: url, + timeout: timeout, + } +} + +func (c *lumsClient) getRootOrgs(apiKey string) (*model.OrgMap, error) { + ctx, cancel := context.WithTimeout(context.Background(), c.timeout) + defer cancel() + url := fmt.Sprintf("%s/sdk/rootOrg", c.baseURL) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + req.Header.Set("Authorization", fmt.Sprintf("token %s", apiKey)) + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to make request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + var errResp model.ErrorResponse + if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil { + return nil, fmt.Errorf("failed to decode error response: %v", err) + } + return nil, fmt.Errorf("%s: %s", errResp.Title, errResp.Message) + } + + var successResp model.SuccessResponse + err = json.NewDecoder(resp.Body).Decode(&successResp) + if err != nil { + return nil, fmt.Errorf("failed to decode success response: %v", err) + } + + return &successResp.Data, nil +} + +// GetRootOrgs is a convenience function that uses the default HTTP client +func GetRootOrgs(apiKey string, url string, timeout time.Duration) (*model.OrgMap, error) { + client := newLumsClient(http.DefaultClient, url, timeout) + result, err := client.getRootOrgs(apiKey) + if err != nil { + return nil, err + } + return result, nil +}