diff --git a/README.md b/README.md index b7284c1..efcbe95 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,6 @@ -# Confidence OpenFeature Go Provider +# Confidence Go SDK -This repo contains the OpenFeature Go flag provider for [Confidence](https://confidence.spotify.com/). - -## OpenFeature - -Before starting to use the provider, it can be helpful to read through the general [OpenFeature docs](https://docs.openfeature.dev/) -and get familiar with the concepts. +This repo contains the [Confidence](https://confidence.spotify.com/) Go SDK. ## Adding the dependency @@ -15,6 +10,74 @@ require ( ) ``` + +## Creating and using the SDK + +Below is an example for how to create an instance of the Confidence SDK, and then resolve a flag with a boolean attribute. + +The SDK is configured via `SetAPIConfig(...)` and `*c.NewAPIConfig(...)`, with which you can set the api key for authentication. +Optionally, a custom resolve API url can be configured if, for example, the resolver service is running on a locally deployed side-car (`NewAPIConfigWithUrl(...)`). + + +You can retrieve properties on the flag variant using property dot notation, meaning `test-flag.boolean-key` will retrieve the attribute `boolean-key` on the flag `test-flag`. + +You can also use only the flag name `test-flag` and retrieve all values as a map with `GetObjectFlag()`. + +The flag's schema is validated against the requested data type, and if it doesn't match it will fall back to the default value. + +```go +import ( + c "github.com/spotify/confidence-sdk-go/pkg/confidence" +) + +confidenceSdk := c.NewConfidenceBuilder().SetAPIConfig(*c.NewAPIConfig("clientSecret")).Build() + +confidence.PutContext("targeting_key", "Random_targeting_key") +flagValue := confidence.GetBoolFlag(context.Background(), "test-flag.boolean-key", false).Value +// we can also pull flag values using a Confidence instance with extra context +confidence.WithContext(map[string]interface{}{ + "Something": 343, +}).GetBoolFlag(context.Background(), "test-flag.boolean-key", false).Value +``` + +The flag will be applied immediately, meaning that Confidence will count the targeted user as having received the treatment once they have have been evaluated. + +### Tracking + +Confidence support event tracking through the SDK. The `Track()` function accepts an en event name and a map of arbitrary data connected to the event. +The current context will also be appended to the event data. + +```go +wg := confidence.Track(context.Background(), "checkout-complete", map[string]interface{}{ + "orderId": 1234, + "total": 100.0, + "items": []string{"item1", "item2"}, +}) +wg.Wait() +``` + +## Logging + +Unless specifically configured using the `ConfidenceBuilder` `setLogger()` function; Confidence uses the default instance of [slog](https://pkg.go.dev/log/slog) for logging valuable information during runtime. +When getting started with Confidence, we suggest you configure [slog](https://pkg.go.dev/log/slog) to emit debug level information: +```go +// Set up the logger with the debug log level +var programLevel = new(slog.LevelVar) +programLevel.Set(slog.LevelDebug) +h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: programLevel}) +slog.SetDefault(slog.New(h)) +``` + +## Demo app + +To run the demo app, replace the `CLIENT_SECRET` with client secret setup in the +[Confidence](https://confidence.spotify.com/) console, the flags with existing flags and execute +the app with `cd demo && go run GoDemoApp.go`. + +## Confidence OpenFeature Go Provider + +The SDK can be combined with the [OpenFeature Go SDK](https://github.com/open-feature/go-sdk), the repo also contains an OpenFeature Provider. Before starting to use the provider, it can be helpful to read through the general [OpenFeature docs](https://docs.openfeature.dev/) +and get familiar with the concepts. It's also important to add the underlying OpenFeature SDK dependency: ``` require ( @@ -27,10 +90,8 @@ require ( Below is an example for how to create a OpenFeature client using the Confidence flag provider, and then resolve a flag with a boolean attribute. -The provider is configured via `NewAPIConfig(...)`, with which you can set the api key for authentication. -Optionally, a custom resolve API url can be configured if, for example, the resolver service is running on a locally deployed side-car (`NewAPIConfigWithUrl(...)`). - -The flag will be applied immediately, meaning that Confidence will count the targeted user as having received the treatment. +The Provider constructor accepts a confidence instance: `NewFlagProvider(confidenceSdk)`, please refer to the previous sections +of this readme for more detailed information on how to set that up. You can retrieve attributes on the flag variant using property dot notation, meaning `test-flag.boolean-key` will retrieve the attribute `boolean-key` on the flag `test-flag`. @@ -46,7 +107,7 @@ import ( p "github.com/spotify/confidence-sdk-go/pkg/provider" ) -confidenceSdk := c.NewConfidenceBuilder().SetAPIConfig(c.APIConfig{APIKey: "clientSecret"}).Build() +confidenceSdk := c.NewConfidenceBuilder().SetAPIConfig(*c.NewAPIConfig("clientSecret")).Build() confidenceProvider := p.NewFlagProvider(confidenceSdk) @@ -61,8 +122,4 @@ attributes["user_id"] = "user1" boolValue, error := client.BooleanValue(context.Background(), "test-flag.boolean-key", false, o.NewEvaluationContext("", attributes)) ``` -## Demo app -To run the demo app, replace the `CLIENT_SECRET` with client secret setup in the -[Confidence](https://confidence.spotify.com/) console, the flags with existing flags and execute -the app with `cd demo && go run GoDemoApp.go`. diff --git a/demo/GoDemoApp.go b/demo/GoDemoApp.go index a60d678..2240e7d 100644 --- a/demo/GoDemoApp.go +++ b/demo/GoDemoApp.go @@ -3,22 +3,36 @@ package main import ( "context" "fmt" + "os" + + "golang.org/x/exp/slog" + c "github.com/spotify/confidence-sdk-go/pkg/confidence" ) func main() { - fmt.Println("Fetching the flags...") - confidence := c.NewConfidenceBuilder().SetAPIConfig(*c.NewAPIConfig("API_KEY")).Build() - targetingKey := "Random_targeting_key" - confidence.PutContext("targeting_key", targetingKey) + // Set up the logger with the debug log level + var programLevel = new(slog.LevelVar) + programLevel.Set(slog.LevelDebug) + h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: programLevel}) + slog.SetDefault(slog.New(h)) - colorValue := confidence.GetStringFlag(context.Background(), "hawkflag.color", "defaultValue").Value - messageValue := confidence.GetStringFlag(context.Background(), "hawkflag.message", "defaultValue").Value + confidence := c.NewConfidenceBuilder().SetAPIConfig(*c.NewAPIConfig("CLIENT_SECRET")).Build() + confidence.PutContext("targeting_key", "Random_targeting_key") + withAddedContext := confidence.WithContext(map[string]interface{}{ + "Something": 343, + }) + + // we can pull flag values using a Confidence instance with extra context + fmt.Println("Fetching the flags...") + colorValue := withAddedContext.GetStringFlag(context.Background(), "hawkflag.color", "defaultValue").Value + messageValue := withAddedContext.GetStringFlag(context.Background(), "hawkflag.message", "defaultValue").Value colorYellow := "\033[33m" colorGreen := "\033[32m" colorRed := "\033[31m" + colorDefault := "\033[0m" fmt.Println(" Color --> " + colorValue) @@ -30,8 +44,13 @@ func main() { default: fmt.Println(colorRed, "Message --> "+messageValue) } + fmt.Print(colorDefault, "") - wg := confidence.Track(context.Background(), "page-viewed", map[string]interface{}{}) + wg := confidence.Track(context.Background(), "checkout-complete", map[string]interface{}{ + "orderId": 1234, + "total": 100.0, + "items": []string{"item1", "item2"}, + }) wg.Wait() fmt.Println("Event sent") } diff --git a/demo/go.mod b/demo/go.mod index 200c1de..b1435f3 100644 --- a/demo/go.mod +++ b/demo/go.mod @@ -4,4 +4,6 @@ go 1.22.2 require github.com/spotify/confidence-sdk-go v0.2.3 -replace github.com/spotify/confidence-sdk-go => ../ \ No newline at end of file +require golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect + +replace github.com/spotify/confidence-sdk-go => ../ diff --git a/demo/go.sum b/demo/go.sum index bae2e6a..578f369 100644 --- a/demo/go.sum +++ b/demo/go.sum @@ -1,15 +1,10 @@ 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= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/open-feature/go-sdk v1.10.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/spotify/confidence-sdk-go v0.2.2-0.20240523155258-4bbc177010cc h1:CyQdom204c6w4UjbT63Otk3axZff4mEbeVDF/snvfas= -github.com/spotify/confidence-sdk-go v0.2.2-0.20240523155258-4bbc177010cc/go.mod h1:TYBqx3F0AZO7HZF8Nkf2dWnvVHH91ICo0W6e9wmLsk4= -github.com/spotify/confidence-sdk-go v0.2.3 h1:vJJjWJo6qgpgX+Lg/uiWrtzRw+qaJhwIQRkxwrBJUCA= -github.com/spotify/confidence-sdk-go v0.2.3/go.mod h1:3MInYY3UiHaNToPlL0mTgbWjcwMMGV/4OfbWAiJ2JC0= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE= golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= 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/go.mod b/go.mod index 15124e6..f8aafff 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,12 @@ go 1.19 require ( github.com/open-feature/go-sdk v1.10.0 github.com/stretchr/testify v1.9.0 - golang.org/x/text v0.14.0 + golang.org/x/exp v0.0.0-20240213143201-ec583247a57a ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0e9c2a9..f83ce24 100644 --- a/go.sum +++ b/go.sum @@ -12,7 +12,6 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE= golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/confidence/EventUploader.go b/pkg/confidence/EventUploader.go index 87a8799..ae2373c 100644 --- a/pkg/confidence/EventUploader.go +++ b/pkg/confidence/EventUploader.go @@ -5,6 +5,8 @@ import ( "context" "encoding/json" "net/http" + + "golang.org/x/exp/slog" ) type EventUploader interface { @@ -14,6 +16,7 @@ type EventUploader interface { type HttpEventUploader struct { Client *http.Client Config APIConfig + Logger *slog.Logger } func (e HttpEventUploader) upload(ctx context.Context, request EventBatchRequest) { @@ -31,7 +34,11 @@ func (e HttpEventUploader) upload(ctx context.Context, request EventBatchRequest resp, err := e.Client.Do(req) if err != nil { + e.Logger.Warn("Failed to perform upload request", "error", err) return } + if resp.StatusCode != http.StatusOK { + e.Logger.Warn("Failed to upload event", "status", resp.Status) + } defer resp.Body.Close() } diff --git a/pkg/confidence/confidence.go b/pkg/confidence/confidence.go index d3ff909..f15d330 100644 --- a/pkg/confidence/confidence.go +++ b/pkg/confidence/confidence.go @@ -2,12 +2,16 @@ package confidence import ( "context" + "encoding/json" "fmt" "net/http" + "net/url" "reflect" "strings" "sync" "time" + + "golang.org/x/exp/slog" ) type FlagResolver interface { @@ -30,6 +34,7 @@ type Confidence struct { contextMap map[string]interface{} Config APIConfig ResolveClient ResolveClient + Logger *slog.Logger } func (e Confidence) GetContext() map[string]interface{} { @@ -55,6 +60,11 @@ type ConfidenceBuilder struct { confidence Confidence } +func (e ConfidenceBuilder) SetLogger(logger *slog.Logger) ConfidenceBuilder { + e.confidence.Logger = logger + return e +} + func (e ConfidenceBuilder) SetAPIConfig(config APIConfig) ConfidenceBuilder { e.confidence.Config = config if config.APIResolveBaseUrl == "" { @@ -69,14 +79,18 @@ func (e ConfidenceBuilder) SetResolveClient(client ResolveClient) ConfidenceBuil } func (e ConfidenceBuilder) Build() Confidence { + if e.confidence.Logger == nil { + e.confidence.Logger = slog.Default() + } if e.confidence.ResolveClient == nil { e.confidence.ResolveClient = HttpResolveClient{Client: &http.Client{}, Config: e.confidence.Config} } if e.confidence.EventUploader == nil { - e.confidence.EventUploader = HttpEventUploader{Client: &http.Client{}, Config: e.confidence.Config} + e.confidence.EventUploader = HttpEventUploader{Client: &http.Client{}, Config: e.confidence.Config, Logger: e.confidence.Logger} } e.confidence.contextMap = make(map[string]interface{}) + e.confidence.Logger.Info("Confidence created", "config", e.confidence.Config) return e.confidence } @@ -117,8 +131,10 @@ func (e Confidence) Track(ctx context.Context, eventName string, data map[string SendTime: iso8601Time, Events: []Event{event}, } + e.Logger.Debug("EventUploading started", "eventName", eventName) e.EventUploader.upload(ctx, batch) wg.Done() + e.Logger.Debug("EventUploading completed", "eventName", eventName) }() return &wg } @@ -138,6 +154,7 @@ func (e Confidence) WithContext(context map[string]interface{}) Confidence { contextMap: newMap, Config: e.Config, ResolveClient: e.ResolveClient, + Logger: e.Logger, } } @@ -196,9 +213,18 @@ func (e Confidence) ResolveFlag(ctx context.Context, flag string, defaultValue i Sdk: sdk{Id: SDK_ID, Version: SDK_VERSION}}) if err != nil { + slog.Warn("Error in resolving flag", "flag", flag, "error", err) return processResolveError(err, defaultValue) } + key := url.QueryEscape(e.Config.APIKey) + flagEncoded := url.QueryEscape(flag) + json, err := json.Marshal(e.GetContext()) + if err == nil { + jsonContextEncoded := url.QueryEscape(string(json)) + e.Logger.Debug("See resolves for " + flag + " in Confidence: https://app.confidence.spotify.com/flags/resolver-test?client-key=" + key + "&flag=flags/" + flagEncoded + "&context=" + jsonContextEncoded) + } if len(resp.ResolvedFlags) == 0 { + slog.Debug("Flag not found", "flag", flag) return InterfaceResolutionDetail{ Value: defaultValue, ResolutionDetail: ResolutionDetail{ @@ -213,6 +239,7 @@ func (e Confidence) ResolveFlag(ctx context.Context, flag string, defaultValue i resolvedFlag := resp.ResolvedFlags[0] if resolvedFlag.Flag != requestFlagName { + slog.Warn("Unexpected flag from remote", "flag", resolvedFlag.Flag) return InterfaceResolutionDetail{ Value: defaultValue, ResolutionDetail: ResolutionDetail{ diff --git a/pkg/confidence/confidence_context_test.go b/pkg/confidence/confidence_context_test.go index 1e7d8aa..1305055 100644 --- a/pkg/confidence/confidence_context_test.go +++ b/pkg/confidence/confidence_context_test.go @@ -5,6 +5,8 @@ import ( "reflect" "testing" + "golang.org/x/exp/slog" + "github.com/stretchr/testify/assert" ) @@ -102,6 +104,7 @@ func create_confidence(t *testing.T, response ResolveResponse) *Confidence { Config: config, ResolveClient: MockResolveClient{MockedResponse: response, MockedError: nil, TestingT: t}, contextMap: make(map[string]interface{}), + Logger: slog.Default(), } } @@ -114,5 +117,6 @@ func createConfidenceWithUploader(t *testing.T, response ResolveResponse, upload EventUploader: uploader, ResolveClient: MockResolveClient{MockedResponse: response, MockedError: nil, TestingT: t}, contextMap: make(map[string]interface{}), + Logger: slog.Default(), } } diff --git a/pkg/confidence/confidence_internal_test.go b/pkg/confidence/confidence_internal_test.go index 5801dc4..8d4c060 100644 --- a/pkg/confidence/confidence_internal_test.go +++ b/pkg/confidence/confidence_internal_test.go @@ -5,9 +5,12 @@ import ( "context" "encoding/json" "fmt" - "github.com/stretchr/testify/assert" "reflect" "testing" + + "golang.org/x/exp/slog" + + "github.com/stretchr/testify/assert" ) type MockResolveClient struct { @@ -255,5 +258,6 @@ func newConfidence(apiKey string, client ResolveClient) *Confidence { Config: config, ResolveClient: client, contextMap: make(map[string]interface{}), + Logger: slog.Default(), } }