Skip to content
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

fix(flags): Add configurable flag timeouts #37

Merged
merged 5 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ jobs:
run: go build .

- name: Test
run: go test -v .
run: go test -timeout 30s -v .
14 changes: 12 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ type Config struct {
// timer triggers.
Interval time.Duration

// Interval at which to fetch new feature flags, 5min by default
// Interval at which to fetch new feature flag definitions, 5min by default
DefaultFeatureFlagsPollingInterval time.Duration

// Calculate when feature flags should be polled next. Setting this property
// Timeout for fetching feature flags, 3 seconds by default
FeatureFlagRequestTimeout time.Duration

// Calculate when feature flag definitions should be polled next. Setting this property
// will override DefaultFeatureFlagsPollingInterval.
NextFeatureFlagsPollingTick func() time.Duration

Expand Down Expand Up @@ -98,6 +101,9 @@ const DefaultInterval = 5 * time.Second
// Specifies the default interval at which to fetch new feature flags
const DefaultFeatureFlagsPollingInterval = 5 * time.Minute

// Specifies the default timeout for fetching feature flags
const DefaultFeatureFlagRequestTimeout = 3 * time.Second

// This constant sets the default batch size used by client instances if none
// was explicitly set.
const DefaultBatchSize = 250
Expand Down Expand Up @@ -139,6 +145,10 @@ func makeConfig(c Config) Config {
c.DefaultFeatureFlagsPollingInterval = DefaultFeatureFlagsPollingInterval
}

if c.FeatureFlagRequestTimeout == 0 {
c.FeatureFlagRequestTimeout = DefaultFeatureFlagRequestTimeout
}

if c.Transport == nil {
c.Transport = http.DefaultTransport
}
Expand Down
3 changes: 2 additions & 1 deletion examples/capture.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package main

import (
"fmt"
"github.com/posthog/posthog-go"
"time"

"github.com/posthog/posthog-go"
)

func TestCapture() {
Expand Down
23 changes: 14 additions & 9 deletions examples/featureflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ import (
)

func TestIsFeatureEnabled() {
client, _ := posthog.NewWithConfig("phc_X8B6bhR1QgQKP1WdpFLN82LxLxgZ7WPXDgJyRyvIpib", posthog.Config{
Interval: 30 * time.Second,
BatchSize: 100,
Verbose: true,
PersonalApiKey: "phx_vXZ7AOnFjDrCxfWLyo9V6P0SWLLfXT2d5euy3U0nRGk",
client, _ := posthog.NewWithConfig("phc_36WfBWNJEQcYotMZ7Ui7EWzqKLbIo2LWJFG5fIg1EER", posthog.Config{
Interval: 30 * time.Second,
BatchSize: 100,
Verbose: true,
PersonalApiKey: "phx_n79cT52OfsxAWDhZs9j3w67aRoBCZ7l5ksRRKmAi5nr",
Endpoint: "http://localhost:8000",
DefaultFeatureFlagsPollingInterval: 5 * time.Second,
FeatureFlagRequestTimeout: 3 * time.Second,
})
defer client.Close()

Expand All @@ -22,38 +25,40 @@ func TestIsFeatureEnabled() {
DistinctId: "hello",
})

fmt.Println("boolResult:", boolResult)

if boolErr != nil || boolResult == nil {
fmt.Println("error:", boolErr)
return
}

// Simple flag
simpleResult, simpleErr := client.GetFeatureFlag(posthog.FeatureFlagPayload{
Key: "simple-test",
DistinctId: "hello",
})

fmt.Println("simpleResult:", simpleResult)
if simpleErr != nil || simpleResult == false {
fmt.Println("error:", simpleErr)
return
}

// Multivariate flag
variantResult, variantErr := client.GetFeatureFlag(posthog.FeatureFlagPayload{
Key: "multivariate-test",
DistinctId: "hello",
})
fmt.Println("variantResult:", variantResult)
if variantErr != nil || variantResult != "variant-value" {
fmt.Println("error:", variantErr)
return
}

// Multivariate + simple flag
variantResult, variantErr = client.GetFeatureFlag(posthog.FeatureFlagPayload{
Key: "multivariate-simple-test",
DistinctId: "hello",
})
fmt.Println("variantResult:", variantResult)
if variantErr != nil || variantResult == true {
fmt.Println("error:", variantErr)
return
}
}
156 changes: 154 additions & 2 deletions feature_flags_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package posthog

import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"time"

"testing"
)
Expand Down Expand Up @@ -613,9 +616,13 @@ func TestFeatureFlagNullComeIntoPlayOnlyWhenDecideErrorsOut(t *testing.T) {

defer server.Close()

// TODO: Make this nicer, right now if all local evaluation requests fail, we block
// on waiting for atleast one request to happen before returning flags,
// which can be suboptimal
client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
DefaultFeatureFlagsPollingInterval: 5 * time.Second,
})
defer client.Close()

Expand Down Expand Up @@ -3232,3 +3239,148 @@ func TestComplexCohortsWithNegationLocally(t *testing.T) {
t.Error("Should match")
}
}

func TestFlagWithTimeoutExceeded(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/decide") {
time.Sleep(1 * time.Second)
w.Write([]byte(fixture("test-decide-v2.json")))
} else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") {
w.Write([]byte(fixture("feature_flag/test-flag-group-properties.json")))
} else if strings.HasPrefix(r.URL.Path, "/batch/") {
// Ignore batch requests
} else {
t.Error("Unknown request made by library")
}
}))
defer server.Close()

client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
FeatureFlagRequestTimeout: 10 * time.Millisecond,
})
defer client.Close()

isMatch, err := client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "enabled-flag",
DistinctId: "-",
},
)

if err == nil {
t.Error("Expected error")
}
if !strings.Contains(err.Error(), "context deadline exceeded") {
t.Error("Expected context deadline exceeded error")
}
if isMatch != nil {
t.Error("Flag shouldn't match")
}

// get all flags with no local evaluation possible
variants, err := client.GetAllFlags(
FeatureFlagPayloadNoKey{
DistinctId: "-",
Groups: Groups{"company": "posthog"},
},
)

if err == nil {
t.Error("Expected error")
}
if !strings.Contains(err.Error(), "context deadline exceeded") {
t.Error("Expected context deadline exceeded error")
}

if variants == nil || len(variants) != 0 {
t.Error("Flag shouldn't match")
}

// get all flags with partial local evaluation possible
variants, err = client.GetAllFlags(
FeatureFlagPayloadNoKey{
DistinctId: "-",
Groups: Groups{"company": "posthog"},
PersonProperties: NewProperties().Set("region", "USA"),
},
)

if err == nil {
t.Error("Expected error")
}
if !strings.Contains(err.Error(), "context deadline exceeded") {
t.Error("Expected context deadline exceeded error")
}

if variants == nil || len(variants) != 1 || variants["simple-flag"] != true {
t.Error("should return locally evaluated flag")
}

// get all flags with full local evaluation possible
variants, err = client.GetAllFlags(
FeatureFlagPayloadNoKey{
DistinctId: "-",
Groups: Groups{"company": "posthog"},
PersonProperties: NewProperties().Set("region", "USA"),
GroupProperties: map[string]Properties{"company": NewProperties().Set("name", "Project Name 1")},
},
)

if err != nil {
t.Error("Unexpected error")
}
fmt.Println(variants)

if variants == nil || len(variants) != 2 || variants["simple-flag"] != true || variants["group-flag"] != true {
t.Error("should return locally evaluated flag")
}
}

func TestFlagDefinitionsWithTimeoutExceeded(t *testing.T) {

// create buffer to write logs to
var buf bytes.Buffer

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/decide") {
w.Write([]byte(fixture("test-decide-v2.json")))
} else if strings.HasPrefix(r.URL.Path, "/api/feature_flag/local_evaluation") {
time.Sleep(11 * time.Second)
w.Write([]byte(fixture("feature_flag/test-flag-group-properties.json")))
} else if strings.HasPrefix(r.URL.Path, "/batch/") {
// Ignore batch requests
} else {
t.Error("Unknown request made by library")
}
}))
defer server.Close()

client, _ := NewWithConfig("Csyjlnlun3OzyNJAafdlv", Config{
PersonalApiKey: "some very secret key",
Endpoint: server.URL,
FeatureFlagRequestTimeout: 10 * time.Millisecond,
Logger: StdLogger(log.New(&buf, "posthog-test", log.LstdFlags)),
})
defer client.Close()

_, err := client.IsFeatureEnabled(
FeatureFlagPayload{
Key: "enabled-flag",
DistinctId: "-",
},
)
if err != nil {
t.Error("Unexpected error")
}

output := buf.String()
if !strings.Contains(output, "Unable to fetch feature flags") {
t.Error("Expected error fetching flags")
}

if !strings.Contains(output, "context deadline exceeded") {
t.Error("Expected timeout error fetching flags")
}
}
Loading
Loading