-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(flags): fall back to /decide
endpoint for GetFeatureFlag
and GetAllFlags
so that users can use this library without needing a personal API Key
#46
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -311,7 +311,7 @@ func (poller *FeatureFlagsPoller) computeFlagLocally( | |
groupName, exists := poller.groups[fmt.Sprintf("%d", *flag.Filters.AggregationGroupTypeIndex)] | ||
|
||
if !exists { | ||
errMessage := "Flag has unknown group type index" | ||
errMessage := "flag has unknown group type index" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. compiler told me to lowercase error messages |
||
return nil, errors.New(errMessage) | ||
} | ||
|
||
|
@@ -467,7 +467,7 @@ func matchCohort(property FlagProperty, properties Properties, cohorts map[strin | |
cohortId := fmt.Sprint(property.Value) | ||
propertyGroup, ok := cohorts[cohortId] | ||
if !ok { | ||
return false, fmt.Errorf("Can't match cohort: cohort %s not found", cohortId) | ||
return false, fmt.Errorf("can't match cohort: cohort %s not found", cohortId) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. compiler told me to lowercase error messages |
||
} | ||
|
||
return matchPropertyGroup(propertyGroup, properties, cohorts) | ||
|
@@ -578,7 +578,7 @@ func matchProperty(property FlagProperty, properties Properties) (bool, error) { | |
return false, &InconclusiveMatchError{"Can't match properties with operator is_not_set"} | ||
} | ||
|
||
override_value, _ := properties[key] | ||
override_value := properties[key] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. compiler told me this was unused. |
||
|
||
if operator == "exact" { | ||
switch t := value.(type) { | ||
|
@@ -637,7 +637,7 @@ func matchProperty(property FlagProperty, properties Properties) (bool, error) { | |
valueString = strconv.Itoa(valueInt) | ||
r, err = regexp.Compile(valueString) | ||
} else { | ||
errMessage := "Regex expression not allowed" | ||
errMessage := "regex expression not allowed" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. compiler told me to lowercase error messages |
||
return false, errors.New(errMessage) | ||
} | ||
|
||
|
@@ -653,7 +653,7 @@ func matchProperty(property FlagProperty, properties Properties) (bool, error) { | |
valueString = strconv.Itoa(valueInt) | ||
match = r.MatchString(valueString) | ||
} else { | ||
errMessage := "Value type not supported" | ||
errMessage := "value type not supported" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. compiler told me to lowercase error messages |
||
return false, errors.New(errMessage) | ||
} | ||
|
||
|
@@ -707,12 +707,12 @@ func matchProperty(property FlagProperty, properties Properties) (bool, error) { | |
func validateOrderable(firstValue interface{}, secondValue interface{}) (float64, float64, error) { | ||
convertedFirstValue, err := interfaceToFloat(firstValue) | ||
if err != nil { | ||
errMessage := "Value 1 is not orderable" | ||
errMessage := "value 1 is not orderable" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. compiler told me to lowercase error messages |
||
return 0, 0, errors.New(errMessage) | ||
} | ||
convertedSecondValue, err := interfaceToFloat(secondValue) | ||
if err != nil { | ||
errMessage := "Value 2 is not orderable" | ||
errMessage := "value 2 is not orderable" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. compiler told me to lowercase error messages |
||
return 0, 0, errors.New(errMessage) | ||
} | ||
|
||
|
@@ -809,7 +809,7 @@ func (poller *FeatureFlagsPoller) GetFeatureFlags() ([]FeatureFlag, error) { | |
_, closed := <-poller.loaded | ||
if closed && poller.featureFlags == nil { | ||
// There was an error with initial flag fetching | ||
return nil, fmt.Errorf("Flags were not successfully fetched yet") | ||
return nil, fmt.Errorf("flags were not successfully fetched yet") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. compiler told me to lowercase error messages |
||
} | ||
|
||
return poller.featureFlags, nil | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ import ( | |
"io" | ||
"io/ioutil" | ||
"net/http" | ||
"net/url" | ||
"sync" | ||
"time" | ||
) | ||
|
@@ -44,14 +45,19 @@ type Client interface { | |
// if the given flag is on or off for the user | ||
GetFeatureFlag(FeatureFlagPayload) (interface{}, error) | ||
// | ||
// Get all flags - returns all flags for a user | ||
GetAllFlags(FeatureFlagPayloadNoKey) (map[string]interface{}, error) | ||
// | ||
// Method forces a reload of feature flags | ||
// NB: This is only available when using a PersonalApiKey | ||
ReloadFeatureFlags() error | ||
// | ||
// Get feature flags - for testing only | ||
// NB: This is only available when using a PersonalApiKey | ||
GetFeatureFlags() ([]FeatureFlag, error) | ||
// | ||
// Get all flags - returns all flags for a user | ||
GetAllFlags(FeatureFlagPayloadNoKey) (map[string]interface{}, error) | ||
// Get the last captured event | ||
GetLastCapturedEvent() *Capture | ||
} | ||
|
||
type client struct { | ||
|
@@ -79,6 +85,11 @@ type client struct { | |
featureFlagsPoller *FeatureFlagsPoller | ||
|
||
distinctIdsFeatureFlagsReported *SizeLimitedMap | ||
|
||
// Last captured event | ||
lastCapturedEvent *Capture | ||
// Mutex to protect last captured event | ||
lastEventMutex sync.RWMutex | ||
} | ||
|
||
// Instantiate a new client that uses the write key passed as first argument to | ||
|
@@ -216,6 +227,7 @@ func (c *client) Enqueue(msg Message) (err error) { | |
} | ||
m.Properties["$active_feature_flags"] = featureKeys | ||
} | ||
c.setLastCapturedEvent(m) | ||
msg = m | ||
|
||
default: | ||
|
@@ -238,17 +250,28 @@ func (c *client) Enqueue(msg Message) (err error) { | |
return | ||
} | ||
|
||
func (c *client) setLastCapturedEvent(event Capture) { | ||
c.lastEventMutex.Lock() | ||
defer c.lastEventMutex.Unlock() | ||
c.lastCapturedEvent = &event | ||
} | ||
|
||
func (c *client) GetLastCapturedEvent() *Capture { | ||
c.lastEventMutex.RLock() | ||
defer c.lastEventMutex.RUnlock() | ||
if c.lastCapturedEvent == nil { | ||
return nil | ||
} | ||
// Return a copy to avoid data races | ||
eventCopy := *c.lastCapturedEvent | ||
return &eventCopy | ||
} | ||
Comment on lines
+253
to
+268
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a handy method for tracking captured events, but I mostly only used it for testing. Perhaps I shouldn't expose it on the API client? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. eh, should be fine. Doesn't seem too unsafe to expose |
||
|
||
func (c *client) IsFeatureEnabled(flagConfig FeatureFlagPayload) (interface{}, error) { | ||
if err := flagConfig.validate(); err != nil { | ||
return false, err | ||
} | ||
|
||
if c.featureFlagsPoller == nil { | ||
errorMessage := "specifying a PersonalApiKey is required for using feature flags" | ||
c.Errorf(errorMessage) | ||
return false, errors.New(errorMessage) | ||
} | ||
Comment on lines
-246
to
-250
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this method falls back to |
||
|
||
result, err := c.GetFeatureFlag(flagConfig) | ||
if err != nil { | ||
return nil, err | ||
|
@@ -278,12 +301,17 @@ func (c *client) GetFeatureFlag(flagConfig FeatureFlagPayload) (interface{}, err | |
return false, err | ||
} | ||
|
||
if c.featureFlagsPoller == nil { | ||
errorMessage := "specifying a PersonalApiKey is required for using feature flags" | ||
c.Errorf(errorMessage) | ||
return "false", errors.New(errorMessage) | ||
var flagValue interface{} | ||
var err error | ||
|
||
if c.featureFlagsPoller != nil { | ||
// get feature flags from the poller, which uses the personal api key | ||
flagValue, err = c.featureFlagsPoller.GetFeatureFlag(flagConfig) | ||
} else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should I log that we're falling back here? Does it matter? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe, but only at a trace/info/debug level. Might be good for investigations. |
||
// if there's no poller, get the feature flag from the decide endpoint | ||
flagValue, err = c.getFeatureFlagFromDecide(flagConfig.Key, flagConfig.DistinctId, flagConfig.Groups, flagConfig.PersonProperties, flagConfig.GroupProperties) | ||
} | ||
flagValue, err := c.featureFlagsPoller.GetFeatureFlag(flagConfig) | ||
|
||
if *flagConfig.SendFeatureFlagEvents && !c.distinctIdsFeatureFlagsReported.contains(flagConfig.DistinctId, flagConfig.Key) { | ||
c.Enqueue(Capture{ | ||
DistinctId: flagConfig.DistinctId, | ||
|
@@ -296,6 +324,7 @@ func (c *client) GetFeatureFlag(flagConfig FeatureFlagPayload) (interface{}, err | |
}) | ||
c.distinctIdsFeatureFlagsReported.add(flagConfig.DistinctId, flagConfig.Key) | ||
} | ||
|
||
return flagValue, err | ||
} | ||
|
||
|
@@ -314,12 +343,16 @@ func (c *client) GetAllFlags(flagConfig FeatureFlagPayloadNoKey) (map[string]int | |
return nil, err | ||
} | ||
|
||
if c.featureFlagsPoller == nil { | ||
errorMessage := "specifying a PersonalApiKey is required for using feature flags" | ||
c.Errorf(errorMessage) | ||
return nil, errors.New(errorMessage) | ||
var flagsValue map[string]interface{} | ||
var err error | ||
|
||
if c.featureFlagsPoller != nil { | ||
flagsValue, err = c.featureFlagsPoller.GetAllFlags(flagConfig) | ||
} else { | ||
flagsValue, err = c.getAllFeatureFlagsFromDecide(flagConfig.DistinctId, flagConfig.Groups, flagConfig.PersonProperties, flagConfig.GroupProperties) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. didn't add the comments here that the above method has, I'll add them back in for cleanliness |
||
} | ||
return c.featureFlagsPoller.GetAllFlags(flagConfig) | ||
|
||
return flagsValue, err | ||
} | ||
|
||
// Close and flush metrics. | ||
|
@@ -336,7 +369,7 @@ func (c *client) Close() (err error) { | |
return | ||
} | ||
|
||
// Asychronously send a batched requests. | ||
// Asynchronously send a batched requests. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. drive-by spellcheck |
||
func (c *client) sendAsync(msgs []message, wg *sync.WaitGroup, ex *executor) { | ||
wg.Add(1) | ||
|
||
|
@@ -558,3 +591,77 @@ func (c *client) getFeatureVariants(distinctId string, groups Groups, personProp | |
} | ||
return featureVariants, nil | ||
} | ||
|
||
func (c *client) makeDecideRequest(distinctId string, groups Groups, personProperties Properties, groupProperties map[string]Properties) (*DecideResponse, error) { | ||
requestData := DecideRequestData{ | ||
ApiKey: c.key, | ||
DistinctId: distinctId, | ||
Groups: groups, | ||
PersonProperties: personProperties, | ||
GroupProperties: groupProperties, | ||
} | ||
|
||
requestDataBytes, err := json.Marshal(requestData) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to marshal decide endpoint request data: %v", err) | ||
} | ||
|
||
decideEndpoint := "decide/?v=2" | ||
url, err := url.Parse(c.Endpoint + "/" + decideEndpoint) | ||
if err != nil { | ||
return nil, fmt.Errorf("creating url: %v", err) | ||
} | ||
|
||
req, err := http.NewRequest("POST", url.String(), bytes.NewReader(requestDataBytes)) | ||
if err != nil { | ||
return nil, fmt.Errorf("creating request: %v", err) | ||
} | ||
|
||
req.Header.Set("Content-Type", "application/json") | ||
req.Header.Set("User-Agent", "posthog-go (version: "+Version+")") | ||
|
||
res, err := c.http.Do(req) | ||
if err != nil { | ||
return nil, fmt.Errorf("sending request: %v", err) | ||
} | ||
defer res.Body.Close() | ||
|
||
if res.StatusCode != http.StatusOK { | ||
return nil, fmt.Errorf("unexpected status code from /decide/: %d", res.StatusCode) | ||
} | ||
|
||
resBody, err := ioutil.ReadAll(res.Body) | ||
if err != nil { | ||
return nil, fmt.Errorf("error reading response from /decide/: %v", err) | ||
} | ||
|
||
var decideResponse DecideResponse | ||
err = json.Unmarshal(resBody, &decideResponse) | ||
if err != nil { | ||
return nil, fmt.Errorf("error parsing response from /decide/: %v", err) | ||
} | ||
|
||
return &decideResponse, nil | ||
} | ||
|
||
func (c *client) getFeatureFlagFromDecide(key string, distinctId string, groups Groups, personProperties Properties, groupProperties map[string]Properties) (interface{}, error) { | ||
decideResponse, err := c.makeDecideRequest(distinctId, groups, personProperties, groupProperties) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if value, ok := decideResponse.FeatureFlags[key]; ok { | ||
return value, nil | ||
} | ||
|
||
return false, nil | ||
} | ||
|
||
func (c *client) getAllFeatureFlagsFromDecide(distinctId string, groups Groups, personProperties Properties, groupProperties map[string]Properties) (map[string]interface{}, error) { | ||
decideResponse, err := c.makeDecideRequest(distinctId, groups, personProperties, groupProperties) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return decideResponse.FeatureFlags, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
compiler told me that
Properties
field was unused.