Skip to content

Commit

Permalink
feat!: Total confidence (#48)
Browse files Browse the repository at this point in the history
* flag api total confidence

* update workflows

* add basic event tracking for confidence and add contextual tests

add tests for contextuals

move context calculations out of a go routine

* add possibility of waiting over a track event

* add demo and demo open feature

* fixup! add demo and demo open feature

* fix linting and spacing and formatting

* fixup! fix linting and spacing and formatting

* fix: flag api confidence v2 (#49)

fix: shadowing a context key should remove it

---------

Co-authored-by: Nicklas Lundin <[email protected]>
  • Loading branch information
vahidlazio and nicklasl authored May 23, 2024
1 parent a9f0a20 commit 51cb84e
Show file tree
Hide file tree
Showing 27 changed files with 1,707 additions and 755 deletions.
31 changes: 27 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,31 @@ jobs:
with:
go-version: '1.20'

- name: Build
run: cd pkg/provider && go build -v .
- name: Build Provider
run: cd provider && go build -v .

- name: Test
run: cd pkg/provider && go test -v
- name: Build Confidence
run: cd confidence && go build -v .

- name: Test Provider
run: cd provider && go test -v

- name: Test Confidence
run: cd confidence && go test -v
- name: Run gofmt
run: |
modules=("confidence" "demo" "demo-open-feature" "provider")
fmt_issues=""
for module in "${modules[@]}"; do
fmt_output=$(gofmt -l "$module")
if [ -n "$fmt_output" ]; then
fmt_issues+="$fmt_output"$'\n'
fi
done
if [ -n "$fmt_issues" ]; then
echo "The following files are not properly formatted:"
echo "$fmt_issues"
echo "Please run 'gofmt -w .' in the respective module directories to format your code."
exit 1
fi
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Default ignored files
*/shelf/
*/workspace.xml
# Editor-based HTTP Client requests
*/httpRequests/
# Datasource local storage ignored files
*/dataSources/
*/dataSources.local.xml
*/vcs.xmlg
*/.fleet
33 changes: 33 additions & 0 deletions confidence/EventUploader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package confidence

import (
"bytes"
"context"
"encoding/json"
"net/http"
)

type EventUploader struct {
client http.Client
config APIConfig
}

func (e EventUploader) upload(ctx context.Context, request EventBatchRequest) {
jsonRequest, err := json.Marshal(request)
if err != nil {
return
}

payload := bytes.NewBuffer(jsonRequest)
req, err := http.NewRequestWithContext(ctx,
http.MethodPost, "https://events.eu.confidence.dev/v1/events:publish", payload)
if err != nil {
return
}

resp, err := e.client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
}
218 changes: 218 additions & 0 deletions confidence/confidence.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package confidence

import (
"context"
"fmt"
"net/http"
"reflect"
"strings"
"sync"
"time"
)

type FlagResolver interface {
resolveFlag(ctx context.Context, flag string, defaultValue interface{},
evalCtx map[string]interface{}, expectedKind reflect.Kind) InterfaceResolutionDetail
}

type ContextProvider interface {
GetContext() map[string]interface{}
}

var (
SDK_ID = "SDK_ID_GO_CONFIDENCE"
SDK_VERSION = "0.1.8" // x-release-please-version
)

type Confidence struct {
parent ContextProvider
uploader EventUploader
contextMap map[string]interface{}
Config APIConfig
ResolveClient ResolveClient
}

func (e Confidence) GetContext() map[string]interface{} {
currentMap := map[string]interface{}{}
parentMap := make(map[string]interface{})
if e.parent != nil {
parentMap = e.parent.GetContext()
}
for key, value := range parentMap {
currentMap[key] = value
}
for key, value := range e.contextMap {
if value == nil {
delete(currentMap, key)
} else {
currentMap[key] = value
}
}
return currentMap
}

type ConfidenceBuilder struct {
confidence Confidence
}

func (e ConfidenceBuilder) SetAPIConfig(config APIConfig) ConfidenceBuilder {
e.confidence.Config = config
return e
}

func (e ConfidenceBuilder) SetResolveClient(client ResolveClient) ConfidenceBuilder {
e.confidence.ResolveClient = client
return e
}

func (e ConfidenceBuilder) Build() Confidence {
if e.confidence.ResolveClient == nil {
e.confidence.ResolveClient = HttpResolveClient{Client: &http.Client{}, Config: e.confidence.Config}
}
e.confidence.contextMap = make(map[string]interface{})
return e.confidence
}

func NewConfidenceBuilder() ConfidenceBuilder {
return ConfidenceBuilder{
confidence: Confidence{},
}
}

func (e Confidence) PutContext(key string, value interface{}) {
e.contextMap[key] = value
}

func (e Confidence) Track(ctx context.Context, eventName string, message map[string]interface{}) *sync.WaitGroup {
newMap := e.GetContext()

for key, value := range message {
newMap[key] = value
}

var wg sync.WaitGroup
wg.Add(1)
go func() {
currentTime := time.Now()
iso8601Time := currentTime.Format(time.RFC3339)
event := Event{
EventDefinition: fmt.Sprintf("eventDefinitions/%s", eventName),
EventTime: iso8601Time,
Payload: newMap,
}
batch := EventBatchRequest{
CclientSecret: e.Config.APIKey,
Sdk: sdk{SDK_ID, SDK_VERSION},
SendTime: iso8601Time,
Events: []Event{event},
}
e.uploader.upload(ctx, batch)
wg.Done()
}()
return &wg
}

func (e Confidence) WithContext(context map[string]interface{}) Confidence {
newMap := map[string]interface{}{}
for key, value := range e.GetContext() {
newMap[key] = value
}

for key, value := range context {
newMap[key] = value
}

return Confidence{
parent: &e,
contextMap: newMap,
Config: e.Config,
ResolveClient: e.ResolveClient,
}
}

func (e Confidence) GetBoolFlag(ctx context.Context, flag string, defaultValue bool) BoolResolutionDetail {
resp := e.ResolveFlag(ctx, flag, defaultValue, reflect.Bool)
return ToBoolResolutionDetail(resp, defaultValue)
}

func (e Confidence) GetBoolValue(ctx context.Context, flag string, defaultValue bool) bool {
return e.GetBoolFlag(ctx, flag, defaultValue).Value
}

func (e Confidence) GetIntFlag(ctx context.Context, flag string, defaultValue int64) IntResolutionDetail {
resp := e.ResolveFlag(ctx, flag, defaultValue, reflect.Int64)
return ToIntResolutionDetail(resp, defaultValue)
}

func (e Confidence) GetIntValue(ctx context.Context, flag string, defaultValue int64) int64 {
return e.GetIntFlag(ctx, flag, defaultValue).Value
}

func (e Confidence) GetDoubleFlag(ctx context.Context, flag string, defaultValue float64) FloatResolutionDetail {
resp := e.ResolveFlag(ctx, flag, defaultValue, reflect.Float64)
return ToFloatResolutionDetail(resp, defaultValue)
}

func (e Confidence) GetDoubleValue(ctx context.Context, flag string, defaultValue float64) float64 {
return e.GetDoubleFlag(ctx, flag, defaultValue).Value
}

func (e Confidence) GetStringFlag(ctx context.Context, flag string, defaultValue string) StringResolutionDetail {
resp := e.ResolveFlag(ctx, flag, defaultValue, reflect.String)
return ToStringResolutionDetail(resp, defaultValue)
}

func (e Confidence) GetStringValue(ctx context.Context, flag string, defaultValue string) string {
return e.GetStringFlag(ctx, flag, defaultValue).Value
}

func (e Confidence) GetObjectFlag(ctx context.Context, flag string, defaultValue string) InterfaceResolutionDetail {
resp := e.ResolveFlag(ctx, flag, defaultValue, reflect.Map)
return resp
}

func (e Confidence) GetObjectValue(ctx context.Context, flag string, defaultValue string) interface{} {
return e.GetObjectFlag(ctx, flag, defaultValue).Value
}

func (e Confidence) ResolveFlag(ctx context.Context, flag string, defaultValue interface{}, expectedKind reflect.Kind) InterfaceResolutionDetail {
flagName, propertyPath := splitFlagString(flag)

requestFlagName := fmt.Sprintf("flags/%s", flagName)
resp, err := e.ResolveClient.SendResolveRequest(ctx,
ResolveRequest{ClientSecret: e.Config.APIKey,
Flags: []string{requestFlagName}, Apply: true, EvaluationContext: e.contextMap,
Sdk: sdk{Id: SDK_ID, Version: SDK_VERSION}})

if err != nil {
return processResolveError(err, defaultValue)
}
if len(resp.ResolvedFlags) == 0 {
return InterfaceResolutionDetail{
Value: defaultValue,
ResolutionDetail: ResolutionDetail{
Variant: "",
Reason: ErrorReason,
ErrorCode: FlagNotFoundCode,
ErrorMessage: "Flag not found",
FlagMetadata: nil,
},
}
}

resolvedFlag := resp.ResolvedFlags[0]
if resolvedFlag.Flag != requestFlagName {
return InterfaceResolutionDetail{
Value: defaultValue,
ResolutionDetail: ResolutionDetail{
Variant: "",
Reason: ErrorReason,
ErrorCode: FlagNotFoundCode,
ErrorMessage: fmt.Sprintf("unexpected flag '%s' from remote", strings.TrimPrefix(resolvedFlag.Flag, "flags/")),
FlagMetadata: nil,
},
}
}

return processResolvedFlag(resolvedFlag, defaultValue, expectedKind, propertyPath)
}
50 changes: 50 additions & 0 deletions confidence/confidence_context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package confidence

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestContextIsInConfidenceObject(t *testing.T) {
client := create_confidence(t, templateResponse())
client.PutContext("hello", "hey")
assert.Equal(t, client.GetContext(), map[string]interface{}{"hello": "hey"})
}

func TestWithContextIsInChildContext(t *testing.T) {
client := create_confidence(t, templateResponse())
client.PutContext("hello", "hey")
child := client.WithContext(map[string]interface{}{"west": "world"})
assert.Equal(t, child.GetContext(), map[string]interface{}{"hello": "hey", "west": "world"})
client.PutContext("hello2", "hey2")
assert.Equal(t, child.GetContext(), map[string]interface{}{"hello": "hey", "west": "world", "hello2": "hey2"})
}

func TestChildContextOverrideParentContext(t *testing.T) {
client := create_confidence(t, templateResponse())
client.PutContext("hello", "hey")
child := client.WithContext(map[string]interface{}{"hello": "boom"})
assert.Equal(t, child.GetContext(), map[string]interface{}{"hello": "boom"})
assert.Equal(t, client.GetContext(), map[string]interface{}{"hello": "hey"})
}

func TestChildContextRemoveParentContext(t *testing.T) {
client := create_confidence(t, templateResponse())
client.PutContext("hello", "hey")
child := client.WithContext(map[string]interface{}{})
child.PutContext("hello", nil)
assert.Equal(t, child.GetContext(), map[string]interface{}{})
}

func create_confidence(t *testing.T, response ResolveResponse) *Confidence {
config := APIConfig{
APIKey: "apiKey",
Region: APIRegionGlobal,
}
return &Confidence{
Config: config,
ResolveClient: MockResolveClient{MockedResponse: response, MockedError: nil, TestingT: t},
contextMap: make(map[string]interface{}),
}
}
Loading

0 comments on commit 51cb84e

Please sign in to comment.