pkg/confidence/confidence.go (220 lines of code) (raw):
package confidence
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"reflect"
"strings"
"sync"
"time"
"golang.org/x/exp/slog"
)
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
EventUploader EventUploader
contextMap map[string]interface{}
Config APIConfig
ResolveClient ResolveClient
Logger *slog.Logger
}
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) 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 == "" {
e.confidence.Config.APIResolveBaseUrl = DefaultAPIResolveBaseUrl
}
return e
}
func (e ConfidenceBuilder) SetResolveClient(client ResolveClient) ConfidenceBuilder {
e.confidence.ResolveClient = client
return e
}
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, Logger: e.confidence.Logger}
}
e.confidence.contextMap = make(map[string]interface{})
e.confidence.Logger.Info("Confidence created", "config", e.confidence.Config)
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, data map[string]interface{}) *sync.WaitGroup {
newMap := make(map[string]interface{})
newMap["context"] = e.GetContext()
for key, value := range data {
if key == "context" {
panic("invalid key \"context\" inside the data")
}
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.Logger.Debug("EventUploading started", "eventName", eventName)
e.EventUploader.upload(ctx, batch)
wg.Done()
e.Logger.Debug("EventUploading completed", "eventName", eventName)
}()
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,
Logger: e.Logger,
}
}
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 map[string]interface{}) InterfaceResolutionDetail {
resp := e.ResolveFlag(ctx, flag, defaultValue, reflect.Map)
return resp
}
func (e Confidence) GetObjectValue(ctx context.Context, flag string, defaultValue map[string]interface{}) 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 {
slog.Warn("Error in resolving flag", "flag", flag, "error", err)
return processResolveError(err, defaultValue)
}
key := url.QueryEscape(e.Config.APIKey)
flagEncoded := url.QueryEscape(flagName)
json, err := json.Marshal(e.GetContext())
if err == nil {
jsonContextEncoded := url.QueryEscape(string(json))
e.Logger.Debug("See resolves for " + flagName + " 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{
Variant: "",
Reason: ErrorReason,
ErrorCode: FlagNotFoundCode,
ErrorMessage: "Flag not found",
FlagMetadata: nil,
},
}
}
resolvedFlag := resp.ResolvedFlags[0]
if resolvedFlag.Flag != requestFlagName {
slog.Warn("Unexpected flag from remote", "flag", resolvedFlag.Flag)
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)
}