Skip to content

Commit

Permalink
feat(offline-mode): Add support for offline mode
Browse files Browse the repository at this point in the history
  • Loading branch information
gagantrivedi committed Jan 8, 2024
1 parent 6e93e4e commit 3506375
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 8 deletions.
32 changes: 24 additions & 8 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ type Client struct {
analyticsProcessor *AnalyticsProcessor
defaultFlagHandler func(string) (Flag, error)

client flaghttp.Client
ctxLocalEval context.Context
ctxAnalytics context.Context
log Logger
client flaghttp.Client
ctxLocalEval context.Context
ctxAnalytics context.Context
log Logger
offlineHandler OfflineHandler
}

// NewClient creates instance of Client with given configuration.
Expand Down Expand Up @@ -60,6 +61,17 @@ func NewClient(apiKey string, options ...Option) *Client {

go c.pollEnvironment(c.ctxLocalEval)
}
if c.config.offlineMode && c.offlineHandler == nil {
panic("offline handler must be provided to use offline mode.")
}
if c.defaultFlagHandler != nil && c.offlineHandler != nil {
panic("default flag handler and offline handler cannot be used together.")
}
if c.offlineHandler != nil {
c.environment.Store(c.offlineHandler.GetEnvironment())

}

// Initialize analytics processor
if c.config.enableAnalytics {
c.analyticsProcessor = NewAnalyticsProcessor(c.ctxAnalytics, c.client, c.config.baseURL, nil, c.log)
Expand All @@ -74,7 +86,7 @@ func NewClient(apiKey string, options ...Option) *Client {
// directly, but instead read the asynchronously updated local environment or
// use the default flag handler in case it has not yet been updated.
func (c *Client) GetEnvironmentFlags(ctx context.Context) (f Flags, err error) {
if c.config.localEvaluation {
if c.config.localEvaluation || c.config.offlineMode {
if f, err = c.getEnvironmentFlagsFromEnvironment(); err == nil {
return f, nil
}
Expand All @@ -83,7 +95,9 @@ func (c *Client) GetEnvironmentFlags(ctx context.Context) (f Flags, err error) {
return f, nil
}
}
if c.defaultFlagHandler != nil {
if c.offlineHandler != nil {
return c.getEnvironmentFlagsFromEnvironment()
} else if c.defaultFlagHandler != nil {
return Flags{defaultFlagHandler: c.defaultFlagHandler}, nil
}
return Flags{}, &FlagsmithClientError{msg: fmt.Sprintf("Failed to fetch flags with error: %s", err)}
Expand All @@ -100,7 +114,7 @@ func (c *Client) GetEnvironmentFlags(ctx context.Context) (f Flags, err error) {
// directly, but instead read the asynchronously updated local environment or
// use the default flag handler in case it has not yet been updated.
func (c *Client) GetIdentityFlags(ctx context.Context, identifier string, traits []*Trait) (f Flags, err error) {
if c.config.localEvaluation {
if c.config.localEvaluation || c.config.offlineMode {
if f, err = c.getIdentityFlagsFromEnvironment(identifier, traits); err == nil {
return f, nil
}
Expand All @@ -109,7 +123,9 @@ func (c *Client) GetIdentityFlags(ctx context.Context, identifier string, traits
return f, nil
}
}
if c.defaultFlagHandler != nil {
if c.offlineHandler != nil {
return c.getIdentityFlagsFromEnvironment(identifier, traits)
} else if c.defaultFlagHandler != nil {
return Flags{defaultFlagHandler: c.defaultFlagHandler}, nil
}
return Flags{}, &FlagsmithClientError{msg: fmt.Sprintf("Failed to fetch flags with error: %s", err)}
Expand Down
104 changes: 104 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,45 @@ func TestClientErrorsIfLocalEvaluationWithNonServerSideKey(t *testing.T) {
})
}

func TestClientErrorsIfOfflineModeWithoutOfflineHandler(t *testing.T) {
// When
defer func() {
if r := recover(); r != nil {
// Then
errMsg := fmt.Sprintf("%v", r)
expectedErrMsg := "offline handler must be provided to use offline mode."
assert.Equal(t, expectedErrMsg, errMsg, "Unexpected error message")
}
}()

// Trigger panic
_ = flagsmith.NewClient("key", flagsmith.WithOfflineMode())
}

func TestClientErrorsIfDefaultHandlerAndOfflineHandlerAreBothSet(t *testing.T) {
// Given
envJsonPath := "./fixtures/environment.json"
offlineHandler, err := flagsmith.NewLocalFileHandler(envJsonPath)
assert.NoError(t, err)

// When
defer func() {
if r := recover(); r != nil {
// Then
errMsg := fmt.Sprintf("%v", r)
expectedErrMsg := "default flag handler and offline handler cannot be used together."
assert.Equal(t, expectedErrMsg, errMsg, "Unexpected error message")
}
}()

// Trigger panic
_ = flagsmith.NewClient("key",
flagsmith.WithOfflineHandler(offlineHandler),
flagsmith.WithDefaultHandler(func(featureName string) (flagsmith.Flag, error) {
return flagsmith.Flag{IsDefault: true}, nil
}))
}

func TestClientUpdatesEnvironmentOnStartForLocalEvaluation(t *testing.T) {
// Given
ctx := context.Background()
Expand Down Expand Up @@ -498,3 +537,68 @@ func TestWithProxyClientOption(t *testing.T) {
assert.Equal(t, fixtures.Feature1ID, allFlags[0].FeatureID)
assert.Equal(t, fixtures.Feature1Value, allFlags[0].Value)
}

func TestOfflineMode(t *testing.T) {
// Given
ctx := context.Background()

envJsonPath := "./fixtures/environment.json"
offlineHandler, err := flagsmith.NewLocalFileHandler(envJsonPath)
assert.NoError(t, err)

client := flagsmith.NewClient(fixtures.EnvironmentAPIKey, flagsmith.WithOfflineMode(), flagsmith.WithOfflineHandler(offlineHandler))

// Then
flags, err := client.GetEnvironmentFlags(ctx)
assert.NoError(t, err)

allFlags := flags.AllFlags()

assert.Equal(t, 1, len(allFlags))

assert.Equal(t, fixtures.Feature1Name, allFlags[0].FeatureName)
assert.Equal(t, fixtures.Feature1ID, allFlags[0].FeatureID)
assert.Equal(t, fixtures.Feature1Value, allFlags[0].Value)

// And GetIdentityFlags should work as well
flags, err = client.GetIdentityFlags(ctx, "test_identity", nil)
assert.NoError(t, err)

allFlags = flags.AllFlags()

assert.Equal(t, 1, len(allFlags))

assert.Equal(t, fixtures.Feature1Name, allFlags[0].FeatureName)
assert.Equal(t, fixtures.Feature1ID, allFlags[0].FeatureID)
assert.Equal(t, fixtures.Feature1Value, allFlags[0].Value)

}

func TestOfflineHandlerIsUsedWhenRequestFails(t *testing.T) {
// Given
ctx := context.Background()

envJsonPath := "./fixtures/environment.json"
offlineHandler, err := flagsmith.NewLocalFileHandler(envJsonPath)
assert.NoError(t, err)

server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()

client := flagsmith.NewClient(fixtures.EnvironmentAPIKey, flagsmith.WithOfflineHandler(offlineHandler),
flagsmith.WithBaseURL(server.URL+"/api/v1/"))

// Then
flags, err := client.GetEnvironmentFlags(ctx)
assert.NoError(t, err)

allFlags := flags.AllFlags()

assert.Equal(t, 1, len(allFlags))

assert.Equal(t, fixtures.Feature1Name, allFlags[0].FeatureName)
assert.Equal(t, fixtures.Feature1ID, allFlags[0].FeatureID)
assert.Equal(t, fixtures.Feature1Value, allFlags[0].Value)
}
1 change: 1 addition & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type config struct {
localEvaluation bool
envRefreshInterval time.Duration
enableAnalytics bool
offlineMode bool
}

// defaultConfig returns default configuration.
Expand Down
58 changes: 58 additions & 0 deletions fixtures/environment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"api_key": "B62qaMZNwfiqT76p38ggrQ",
"project": {
"name": "Test project",
"organisation": {
"feature_analytics": false,
"name": "Test Org",
"id": 1,
"persist_trait_data": true,
"stop_serving_flags": false
},
"id": 1,
"hide_disabled_flags": false,
"segments": [
{
"id": 1,
"name": "Test Segment",
"feature_states": [],
"rules": [
{
"type": "ALL",
"conditions": [],
"rules": [
{
"type": "ALL",
"rules": [],
"conditions": [
{
"operator": "EQUAL",
"property_": "foo",
"value": "bar"
}
]
}
]
}
]
}
]
},
"segment_overrides": [],
"id": 1,
"feature_states": [
{
"multivariate_feature_state_values": [],
"feature_state_value": "some_value",
"id": 1,
"featurestate_uuid": "40eb539d-3713-4720-bbd4-829dbef10d51",
"feature": {
"name": "feature_1",
"type": "STANDARD",
"id": 1
},
"segment_id": null,
"enabled": true
}
]
}
39 changes: 39 additions & 0 deletions offline_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package flagsmith

import (
"encoding/json"
"os"

"github.com/Flagsmith/flagsmith-go-client/v3/flagengine/environments"
)

type OfflineHandler interface {
GetEnvironment() *environments.EnvironmentModel
}

type LocalFileHandler struct {
environment *environments.EnvironmentModel
}

func NewLocalFileHandler(environmentDocumentPath string) (*LocalFileHandler, error) {
// Read the environment document from the specified path
environmentDocument, err := os.ReadFile(environmentDocumentPath)
if err != nil {
return nil, err
}
var environment environments.EnvironmentModel
if err := json.Unmarshal(environmentDocument, &environment); err != nil {
return nil, err
}

// Create and initialize the LocalFileHandler
handler := &LocalFileHandler{
environment: &environment,
}

return handler, nil
}

func (handler *LocalFileHandler) GetEnvironment() *environments.EnvironmentModel {
return handler.environment
}
35 changes: 35 additions & 0 deletions offline_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package flagsmith_test

import (
"testing"

flagsmith "github.com/Flagsmith/flagsmith-go-client/v3"
"github.com/stretchr/testify/assert"
)

func TestNewLocalFileHandler(t *testing.T) {
// Given
envJsonPath := "./fixtures/environment.json"

// When
offlineHandler, err := flagsmith.NewLocalFileHandler(envJsonPath)

// Then
assert.NoError(t, err)
assert.NotNil(t, offlineHandler)

}

func TestLocalFileHandlerGetEnvironment(t *testing.T) {
// Given
envJsonPath := "./fixtures/environment.json"
localHandler, err := flagsmith.NewLocalFileHandler(envJsonPath)

assert.NoError(t, err)

// When
environment := localHandler.GetEnvironment()

// Then
assert.NotNil(t, environment.APIKey)
}
11 changes: 11 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,14 @@ func WithProxy(proxyURL string) Option {
c.client.SetProxy(proxyURL)
}
}

func WithOfflineHandler(handler OfflineHandler) Option {
return func(c *Client) {
c.offlineHandler = handler
}
}
func WithOfflineMode() Option {
return func(c *Client) {
c.config.offlineMode = true
}
}

0 comments on commit 3506375

Please sign in to comment.