diff --git a/README.md b/README.md index aaf1db2..a7bf6fa 100644 --- a/README.md +++ b/README.md @@ -1 +1,174 @@ -# openfeature-go-server-sdk \ No newline at end of file +# Bucketeer - OpenFeature Go server provider + +This is the official Go OpenFeature provider for accessing your feature flags with [Bucketeer](https://bucketeer.io/). + +[Bucketeer](https://bucketeer.io) is an open-source platform created by [CyberAgent](https://www.cyberagent.co.jp/en/) to help teams make better decisions, reduce deployment lead time and release risk through feature flags. Bucketeer offers advanced features like dark launches and staged rollouts that perform limited releases based on user attributes, devices, and other segments. + +In conjunction with the [OpenFeature SDK](https://openfeature.dev/docs/reference/concepts/provider) you will be able to evaluate your feature flags in your **server-side** applications. + +> [!WARNING] +> This is a beta version. Breaking changes may be introduced before general release. + +For documentation related to flags management in Bucketeer, refer to the [Bucketeer documentation website](https://docs.bucketeer.io/sdk/server-side/go). + +## Supported Go versions + +Minimum requirements: + +| Tool | Version | +| ----- | ------- | +| Go | 1.21+ | + +## Installation + +```bash +go get github.com/bucketeer-io/openfeature-go-server-sdk +``` + +## Usage + +### Initialize the provider + +Bucketeer provider needs to be created and then set in the global OpenFeatureAPI. + +```go +import ( + "context" + "github.com/bucketeer-io/go-server-sdk/pkg/bucketeer" + provider "github.com/bucketeer-io/openfeature-go-server-sdk/pkg" + "github.com/open-feature/go-sdk/openfeature" +) + +func main() { + // SDK configuration + options := provider.ProviderOptions{ + bucketeer.WithAPIKey("YOUR_API_KEY"), + bucketeer.WithAPIEndpoint("YOUR_API_ENDPOINT"), + bucketeer.WithTag("YOUR_FEATURE_TAG"), + // Add other options as needed + } + + // Create provider + p, err := provider.NewProviderWithContext(context.Background(), options) + if err != nil { + // Error handling + } + + // User configuration + userID := "targetingUserId" + evalCtx := openfeature.FlattenedContext{ + openfeature.TargetingKey: userID, + // Add other attributes as needed + } + // Evaluate feature flag + result := p.BooleanEvaluation(context.Background(), "feature-flag-id", false, evalCtx) + if result.Error() != nil { + // Handle error + } +} +``` + +### Evaluate a feature flag + +The Bucketeer provider supports evaluating different types of feature flags. Each evaluation method returns a resolution detail object containing the evaluated value and additional metadata. + +#### Boolean Evaluation + +```go +result := p.BooleanEvaluation(context.Background(), "bool-feature-flag", false, evalCtx) +if result.Error() != nil { + // Handle error +} +// Access the evaluated value +boolValue := result.Value +``` + +#### String Evaluation + +```go +result := p.StringEvaluation(context.Background(), "string-feature-flag", "default-value", evalCtx) +if result.Error() != nil { + // Handle error +} +// Access the evaluated value and variant +stringValue := result.Value +variant := result.Variant +``` + +#### Integer Evaluation + +```go +result := p.IntEvaluation(context.Background(), "int-feature-flag", 100, evalCtx) +if result.Error() != nil { + // Handle error +} +// Access the evaluated value +intValue := result.Value +``` + +#### Float Evaluation + +```go +result := p.FloatEvaluation(context.Background(), "float-feature-flag", 3.14, evalCtx) +if result.Error() != nil { + // Handle error +} +// Access the evaluated value +floatValue := result.Value +``` + +#### Object Evaluation + +```go +defaultObject := map[string]interface{}{ + "key": "default-value", +} +result := p.ObjectEvaluation(context.Background(), "object-feature-flag", defaultObject, evalCtx) +if result.Error() != nil { + // Handle error +} +// Access the evaluated value +objectValue := result.Value +``` + +See our [documentation](https://docs.bucketeer.io/sdk/server-side/go) for more SDK configuration. + +The evaluation context allows the client to specify contextual data that Bucketeer uses to evaluate the feature flags. + +The `targetingKey` is the user ID (Unique ID) and cannot be empty. + +## Example + +Check out the [example directory](./example) for a complete working example of how to use this SDK in a web application. + +## Testing + +### Unit Tests + +To run unit tests: + +```bash +make test +``` + +### E2E Tests + +```bash +export API_KEY="YOUR_API_KEY" +export API_ENDPOINT="YOUR_API_ENDPOINT" +export TAG="YOUR_FEATURE_TAG" # optional +export SCHEME="https" # optional +make e2e +``` + +For more details, see the [E2E Test README](./test/e2e/README.md). + +## Contributing + +We would ❤️ for you to contribute to Bucketeer and help improve it! Anyone can use and enjoy it! + +Please follow our contribution guide [here](https://docs.bucketeer.io/contribution-guide/). + +## License + +Apache License 2.0, see [LICENSE](https://github.com/bucketeer-io/openfeature-go-server-sdk/blob/main/LICENSE). diff --git a/example/go.mod b/example/go.mod new file mode 100644 index 0000000..051f9d8 --- /dev/null +++ b/example/go.mod @@ -0,0 +1,27 @@ +module github.com/bucketeer-io/openfeature-go-server-sdk/example + +go 1.24.2 + +require ( + github.com/bucketeer-io/go-server-sdk v1.5.6-0.20250515121938-9e1d5ff17a1d + github.com/bucketeer-io/openfeature-go-server-sdk v0.0.0-20250516033446-aaf587c291ff + github.com/open-feature/go-sdk v1.14.1 +) + +require ( + github.com/blang/semver v3.5.1+incompatible // indirect + github.com/bucketeer-io/bucketeer v1.3.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/jinzhu/copier v0.4.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + go.opencensus.io v0.24.0 // indirect + golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.66.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect +) diff --git a/example/go.sum b/example/go.sum new file mode 100644 index 0000000..d29dd9f --- /dev/null +++ b/example/go.sum @@ -0,0 +1,141 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bucketeer-io/bucketeer v1.3.0 h1:ge+rc+TfzWMx3VBJbzUQg/1nl/fQFBZYojQ/7JVUYRU= +github.com/bucketeer-io/bucketeer v1.3.0/go.mod h1:StIY3RFNVH5HHftNFizB1sN1TMC9M7MwVyV9y6Dcvso= +github.com/bucketeer-io/go-server-sdk v1.5.6-0.20250515121938-9e1d5ff17a1d h1:i/jzjeGbogQ7PrPLA/JVezbjxl/SOzrxkdjkcv/MXuI= +github.com/bucketeer-io/go-server-sdk v1.5.6-0.20250515121938-9e1d5ff17a1d/go.mod h1:2nqgq8KLjdtCPUv/P9IcO0R33Mt5igjQiXYgqcp2Afs= +github.com/bucketeer-io/openfeature-go-server-sdk v0.0.0-20250516033446-aaf587c291ff h1:CVi1CoX3sWKdK476IWku1YJe6ow31BH3+QAyWLAf5qY= +github.com/bucketeer-io/openfeature-go-server-sdk v0.0.0-20250516033446-aaf587c291ff/go.mod h1:QTkajcEJPou/TR8HnVETqFE4Otc6lEUlYk3E/RbVY8c= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +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= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/open-feature/go-sdk v1.14.1 h1:jcxjCIG5Up3XkgYwWN5Y/WWfc6XobOhqrIwjyDBsoQo= +github.com/open-feature/go-sdk v1.14.1/go.mod h1:t337k0VB/t/YxJ9S0prT30ISUHwYmUd/jhUZgFcOvGg= +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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +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/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20240529005216-23cca8864a10 h1:vpzMC/iZhYFAjJzHU0Cfuq+w1vLLsF2vLkDrPjzKYck= +golang.org/x/exp v0.0.0-20240529005216-23cca8864a10/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..fdee791 --- /dev/null +++ b/example/main.go @@ -0,0 +1,306 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "maps" + "net/http" + "os" + "os/signal" + "strconv" + "time" + + "github.com/bucketeer-io/go-server-sdk/pkg/bucketeer" + "github.com/bucketeer-io/go-server-sdk/pkg/bucketeer/uuid" + provider "github.com/bucketeer-io/openfeature-go-server-sdk/pkg" + "github.com/open-feature/go-sdk/openfeature" +) + +const ( + timeout = 10 * time.Second + userIDKey = "user_id" +) + +var ( + bucketeerTag = flag.String("bucketeer-tag", "", "Bucketeer tag") + bucketeerAPIKey = flag.String("bucketeer-api-key", "", "Bucketeer api key") + bucketeerAPIEndpoint = flag.String("bucketeer-api-endpoint", "", "Bucketeer api endpoint, e.g. api.example.com") + scheme = flag.String("scheme", "https", "Scheme of the Bucketeer service, e.g. https") + booleanFeatureID = flag.String("boolean-feature-id", "example-boolean-flag", "Boolean feature ID") + stringFeatureID = flag.String("string-feature-id", "example-string-flag", "String feature ID") + intFeatureID = flag.String("int-feature-id", "example-int-flag", "Integer feature ID") + floatFeatureID = flag.String("float-feature-id", "example-float-flag", "Float feature ID") + objectFeatureID = flag.String("object-feature-id", "example-object-flag", "Object feature ID") +) + +func main() { + flag.Parse() + + // Set up Bucketeer SDK options + options := provider.ProviderOptions{ + bucketeer.WithTag(*bucketeerTag), + bucketeer.WithAPIKey(*bucketeerAPIKey), + bucketeer.WithAPIEndpoint(*bucketeerAPIEndpoint), + bucketeer.WithScheme(*scheme), + bucketeer.WithEnableDebugLog(true), + } + + // Create OpenFeature provider with Bucketeer SDK + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + p, err := provider.NewProviderWithContext(ctx, options) + if err != nil { + log.Fatalf("Failed to create provider: %v", err) + } + + // Setup and start HTTP server + app := &exampleApp{ + provider: p, + booleanFeatureID: *booleanFeatureID, + stringFeatureID: *stringFeatureID, + intFeatureID: *intFeatureID, + floatFeatureID: *floatFeatureID, + objectFeatureID: *objectFeatureID, + } + + // Run example HTTP server + if err := app.run(":8080"); err != nil { + log.Fatalf("Server error: %v", err) + } +} + +type exampleApp struct { + provider openfeature.FeatureProvider + booleanFeatureID string + stringFeatureID string + intFeatureID string + floatFeatureID string + objectFeatureID string + goalID string +} + +func (a *exampleApp) run(addr string) error { + srv := &http.Server{ + Addr: addr, + Handler: a.routes(), + ReadTimeout: timeout, + WriteTimeout: timeout, + } + + // Graceful shutdown setup + serverCtx, serverStopCtx := context.WithCancel(context.Background()) + + // Listen for interrupt signal + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt) + + go func() { + <-sig + log.Println("Shutdown signal received") + + // Give server timeout to shutdown + shutdownCtx, cancel := context.WithTimeout(serverCtx, 10*time.Second) + defer cancel() + + if err := srv.Shutdown(shutdownCtx); err != nil { + log.Fatalf("Server shutdown error: %v", err) + } + serverStopCtx() + }() + + log.Printf("Server starting on %s", addr) + + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + return err + } + + <-serverCtx.Done() + log.Println("Server stopped") + return nil +} + +func (a *exampleApp) routes() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("/feature/boolean", a.booleanFeatureHandler) + mux.HandleFunc("/feature/string", a.stringFeatureHandler) + mux.HandleFunc("/feature/int", a.intFeatureHandler) + mux.HandleFunc("/feature/float", a.floatFeatureHandler) + mux.HandleFunc("/feature/object", a.objectFeatureHandler) + return mux +} + +func (a *exampleApp) getUserCtx(r *http.Request) openfeature.FlattenedContext { + userID := a.getUserID(r) + + // Extract attributes from query parameters + attributes := make(map[string]interface{}) + for key, values := range r.URL.Query() { + if key != "user_id" && len(values) > 0 { + attributes[key] = values[0] + } + } + + // Create evaluation context with targeting key and attributes + evalCtx := map[string]interface{}{ + openfeature.TargetingKey: userID, + } + + // Add all query parameters as attributes using maps.Copy + maps.Copy(evalCtx, attributes) + + return evalCtx +} + +func (a *exampleApp) getUserID(r *http.Request) string { + // Check if user ID is in query parameters + if userIDParam := r.URL.Query().Get("user_id"); userIDParam != "" { + return userIDParam + } + + // Check if user ID is in cookies + cookie, err := r.Cookie(userIDKey) + if err == nil && cookie.Value != "" { + return cookie.Value + } + + // Generate new user ID + newUUID, err := uuid.NewV4() + if err != nil { + // Fallback to timestamp if UUID generation fails + return strconv.FormatInt(time.Now().UnixNano(), 10) + } + + return newUUID.String() +} + +func (a *exampleApp) booleanFeatureHandler(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + evalCtx := a.getUserCtx(r) + result := a.provider.BooleanEvaluation(ctx, a.booleanFeatureID, false, evalCtx) + + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "featureId": %v, + "value": %t, + "reason": %v, + "userId": %v, + "error": %v +}`, + a.booleanFeatureID, + result.Value, + result.Reason, + evalCtx[openfeature.TargetingKey], + result.Error(), + ) +} + +func (a *exampleApp) stringFeatureHandler(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + evalCtx := a.getUserCtx(r) + result := a.provider.StringEvaluation(ctx, a.stringFeatureID, "default", evalCtx) + + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "featureId": %v, + "value": %v, + "reason": %v, + "variant": %v, + "userId": %v, + "error": %v +}`, + a.stringFeatureID, + result.Value, + result.Reason, + result.Variant, + evalCtx[openfeature.TargetingKey], + result.Error(), + ) +} + +func (a *exampleApp) intFeatureHandler(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + evalCtx := a.getUserCtx(r) + result := a.provider.IntEvaluation(ctx, a.intFeatureID, 0, evalCtx) + + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "featureId": %v, + "value": %v, + "reason": %v, + "variant": %v, + "userId": %v, + "error": %v +}`, + a.intFeatureID, + result.Value, + result.Reason, + result.Variant, + evalCtx[openfeature.TargetingKey], + result.Error(), + ) +} + +func (a *exampleApp) floatFeatureHandler(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + evalCtx := a.getUserCtx(r) + result := a.provider.FloatEvaluation(ctx, a.floatFeatureID, 0.0, evalCtx) + + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "featureId": %v, + "value": %v, + "reason": %v, + "variant": %v, + "userId": %v, + "error": %v +}`, + a.floatFeatureID, + result.Value, + result.Reason, + result.Variant, + evalCtx[openfeature.TargetingKey], + result.Error(), + ) +} + +func (a *exampleApp) objectFeatureHandler(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + type ExampleStruct struct { + Name string `json:"name"` + Value int `json:"value"` + } + + evalCtx := a.getUserCtx(r) + defaultObj := ExampleStruct{Name: "default", Value: 0} + result := a.provider.ObjectEvaluation(ctx, a.objectFeatureID, defaultObj, evalCtx) + + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "featureId": %v, + "value": %v, + "reason": %v, + "variant": %v, + "userId": %v, + "error": %v +}`, + a.objectFeatureID, + result.Value, + result.Reason, + result.Variant, + evalCtx[openfeature.TargetingKey], + result.Error(), + ) +} diff --git a/go.mod b/go.mod index a0e67a5..1e22466 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/bucketeer-io/openfeature-go-server-sdk go 1.24.2 require ( - github.com/bucketeer-io/go-server-sdk v1.5.6-0.20250515121938-9e1d5ff17a1d + github.com/bucketeer-io/go-server-sdk v1.5.6-0.20250611082159-ea1d748fddf8 github.com/open-feature/go-sdk v1.14.1 github.com/stretchr/testify v1.10.0 ) diff --git a/go.sum b/go.sum index f2545da..f64fdbd 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bucketeer-io/bucketeer v1.3.0 h1:ge+rc+TfzWMx3VBJbzUQg/1nl/fQFBZYojQ/7JVUYRU= github.com/bucketeer-io/bucketeer v1.3.0/go.mod h1:StIY3RFNVH5HHftNFizB1sN1TMC9M7MwVyV9y6Dcvso= -github.com/bucketeer-io/go-server-sdk v1.5.6-0.20250515121938-9e1d5ff17a1d h1:i/jzjeGbogQ7PrPLA/JVezbjxl/SOzrxkdjkcv/MXuI= -github.com/bucketeer-io/go-server-sdk v1.5.6-0.20250515121938-9e1d5ff17a1d/go.mod h1:2nqgq8KLjdtCPUv/P9IcO0R33Mt5igjQiXYgqcp2Afs= +github.com/bucketeer-io/go-server-sdk v1.5.6-0.20250611082159-ea1d748fddf8 h1:pwEUcGR6NoSwDamr8f+e8tWa5XepDaDIkiXG7tustgc= +github.com/bucketeer-io/go-server-sdk v1.5.6-0.20250611082159-ea1d748fddf8/go.mod h1:2nqgq8KLjdtCPUv/P9IcO0R33Mt5igjQiXYgqcp2Afs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=