Skip to content
2 changes: 1 addition & 1 deletion infra/sidecar.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# ----- Builder image
ARG GOLANG_VERSION=1.23.6
ARG GOLANG_VERSION=1.24.0
FROM golang:${GOLANG_VERSION}-bookworm AS builder

ARG FIPS_MODE
Expand Down
4 changes: 4 additions & 0 deletions splitd.yaml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ sdk:
apikey: <server-side-apitoken>
labelsEnabled: true
streamingEnabled: true
fallbackTreatment:
global_fallback_treatment:
treatment: other
by_flag_fallback_treatment: {}
urls:
auth: https://auth.split.io
sdk: https://sdk.split.io/api
Expand Down
2 changes: 1 addition & 1 deletion splitio/commitsha.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package splitio

const CommitSHA = "a651b23"
const CommitSHA = "085f07b"
61 changes: 36 additions & 25 deletions splitio/conf/splitcli.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@ type CliArgs struct {
WriteTimeoutMS int

// command
Method string
Key string
BucketingKey string
Feature string
Features []string
TrafficType string
EventType string
EventVal *float64
Attributes map[string]interface{}
Method string
Key string
BucketingKey string
Feature string
Features []string
TrafficType string
EventType string
EventVal *float64
Attributes map[string]interface{}
ImpressionProperties map[string]interface{}
}

func (a *CliArgs) LinkOpts() (*link.ConsumerOptions, error) {
Expand Down Expand Up @@ -85,6 +86,7 @@ func ParseCliArgs() (*CliArgs, error) {
et := cliFlags.String("event-type", "", "event type")
ev := cliFlags.String("value", "", "event associated value")
at := cliFlags.String("attributes", "", "json representation of attributes")
pr := cliFlags.String("impression-properties", "null", "json representation of")
err := cliFlags.Parse(os.Args[1:])
if err != nil {
return nil, fmt.Errorf("error parsing arguments: %w", err)
Expand All @@ -107,22 +109,31 @@ func ParseCliArgs() (*CliArgs, error) {
return nil, fmt.Errorf("error parsing attributes: %w", err)
}

if *pr == "" {
*pr = "null"
}
impressionPorperties := make(map[string]interface{})
if err = json.Unmarshal([]byte(*pr), &impressionPorperties); err != nil {
return nil, fmt.Errorf("error parsing impression properties: %w", err)
}

return &CliArgs{
ID: *id,
Serialization: *s,
Protocol: *p,
LogLevel: *ll,
ConnType: *ct,
ConnAddr: *ca,
BufSize: *bs,
Method: *m,
Key: *k,
BucketingKey: *bk,
Feature: *f,
Features: strings.Split(*fs, ","),
TrafficType: *tt,
EventType: *et,
EventVal: eventVal,
Attributes: attrs,
ID: *id,
Serialization: *s,
Protocol: *p,
LogLevel: *ll,
ConnType: *ct,
ConnAddr: *ca,
BufSize: *bs,
Method: *m,
Key: *k,
BucketingKey: *bk,
Feature: *f,
Features: strings.Split(*fs, ","),
TrafficType: *tt,
EventType: *et,
EventVal: eventVal,
Attributes: attrs,
ImpressionProperties: impressionPorperties,
}, nil
}
2 changes: 2 additions & 0 deletions splitio/conf/splitcli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func TestCliConfig(t *testing.T) {
"-event-type=someEventType",
"-value=0.123",
`-attributes={"some": "attribute"}`,
`-impression-properties={"userId": "123", "age": 30, "premium": true, "balance": 99.5}`,
}

parsed, err := ParseCliArgs()
Expand All @@ -42,6 +43,7 @@ func TestCliConfig(t *testing.T) {
assert.Equal(t, "someEventType", parsed.EventType)
assert.Equal(t, lang.Ref(float64(0.123)), parsed.EventVal)
assert.Equal(t, map[string]interface{}{"some": "attribute"}, parsed.Attributes)
assert.Equal(t, map[string]interface{}{"userId": "123", "age": float64(30), "premium": true, "balance": 99.5}, parsed.ImpressionProperties)

// test bad buffer size
os.Args = []string{os.Args[0], "-buffer-size=sarasa"}
Expand Down
131 changes: 123 additions & 8 deletions splitio/conf/splitd.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"time"

"github.com/splitio/go-split-commons/v9/dtos"
"github.com/splitio/go-toolkit/v5/logging"
"github.com/splitio/splitd/splitio/common/lang"
"github.com/splitio/splitd/splitio/link"
Expand Down Expand Up @@ -122,21 +123,23 @@ func (l *Link) ToListenerOpts() (*link.ListenerOptions, error) {
}

type SDK struct {
Apikey string `yaml:"apikey"`
LabelsEnabled *bool `yaml:"labelsEnabled"`
StreamingEnabled *bool `yaml:"streamingEnabled"`
URLs URLs `yaml:"urls"`
FeatureFlags FeatureFlags `yaml:"featureFlags"`
Impressions Impressions `yaml:"impressions"`
Events Events `yaml:"events"`
FlagSetsFilter []string `yaml:"flagSetsFilter"`
Apikey string `yaml:"apikey"`
LabelsEnabled *bool `yaml:"labelsEnabled"`
StreamingEnabled *bool `yaml:"streamingEnabled"`
FallbackTreatment fallbackTreatmentInput `yaml:"fallbackTreatment"`
URLs URLs `yaml:"urls"`
FeatureFlags FeatureFlags `yaml:"featureFlags"`
Impressions Impressions `yaml:"impressions"`
Events Events `yaml:"events"`
FlagSetsFilter []string `yaml:"flagSetsFilter"`
}

func (s *SDK) PopulateWithDefaults() {
cfg := sdkConf.DefaultConfig()
s.Apikey = apikeyPlaceHolder
s.LabelsEnabled = lang.Ref(cfg.LabelsEnabled)
s.StreamingEnabled = lang.Ref(cfg.StreamingEnabled)
s.FallbackTreatment = fallbackTreatmentFromConfig(cfg.FallbackTreatment)
s.URLs.PopulateWithDefaults()
s.FeatureFlags.PopulateWithDefaults()
s.Impressions.PopulateWithDefaults()
Expand Down Expand Up @@ -216,6 +219,11 @@ func (s *SDK) ToSDKConf() *sdkConf.Config {
if len(s.FlagSetsFilter) > 0 {
cfg.FlagSetsFilter = s.FlagSetsFilter
}
if parsed, err := (&s.FallbackTreatment).toConfig(); err != nil {
log.Printf("[splitd] fallbackTreatment: %v", err)
} else if parsed != nil {
cfg.FallbackTreatment = *parsed
}
return cfg
}

Expand Down Expand Up @@ -310,6 +318,113 @@ func (p *Profiling) PopulateWithDefaults() {
p.Port = 8888
}

// fallbackTreatmentFromConfig maps the SDK default config's FallbackTreatment into our input type.
func fallbackTreatmentFromConfig(c dtos.FallbackTreatmentConfig) fallbackTreatmentInput {
parsed := new(dtos.FallbackTreatmentConfig)
*parsed = c
return fallbackTreatmentInput{parsed: parsed}
}

type fallbackTreatmentEntry struct {
Treatment *string `json:"treatment" yaml:"treatment"`
Config *string `json:"config,omitempty" yaml:"config,omitempty"`
}

type fallbackTreatmentInput struct {
parsed *dtos.FallbackTreatmentConfig
raw string
}

func (f *fallbackTreatmentInput) UnmarshalYAML(value *yaml.Node) error {
if value == nil {
return nil
}
switch value.Kind {
case yaml.ScalarNode:
var s string
if err := value.Decode(&s); err != nil {
return err
}
f.parsed = nil
f.raw = strings.TrimSpace(s)
return nil
case yaml.MappingNode:
var m struct {
Global *fallbackTreatmentEntry `yaml:"global_fallback_treatment"`
ByFlag map[string]fallbackTreatmentEntry `yaml:"by_flag_fallback_treatment"`
}
if err := value.Decode(&m); err != nil {
return err
}
out := dtos.FallbackTreatmentConfig{}
if m.Global != nil && m.Global.Treatment != nil {
out.GlobalFallbackTreatment = &dtos.FallbackTreatment{
Treatment: m.Global.Treatment,
Config: m.Global.Config,
}
}
if len(m.ByFlag) > 0 {
out.ByFlagFallbackTreatment = make(map[string]dtos.FallbackTreatment)
for name, v := range m.ByFlag {
if v.Treatment != nil {
out.ByFlagFallbackTreatment[name] = dtos.FallbackTreatment{
Treatment: v.Treatment,
Config: v.Config,
}
}
}
}
f.parsed = &out
return nil
}
return nil
}

func (f *fallbackTreatmentInput) toConfig() (*dtos.FallbackTreatmentConfig, error) {
if f == nil {
return nil, nil
}
if f.raw != "" {
return parseFallbackTreatmentJSON(f.raw)
}
if f.parsed != nil {
return f.parsed, nil
}
return nil, nil
}

func parseFallbackTreatmentJSON(raw string) (*dtos.FallbackTreatmentConfig, error) {
var wrapper struct {
FallbackTreatment struct {
GlobalFallbackTreatment *fallbackTreatmentEntry `json:"global_fallback_treatment"`
ByFlagFallbackTreatment map[string]fallbackTreatmentEntry `json:"by_flag_fallback_treatment"`
} `json:"fallback_treatment"`
}
if err := json.Unmarshal([]byte(raw), &wrapper); err != nil {
return nil, fmt.Errorf("invalid JSON: %w", err)
}
out := dtos.FallbackTreatmentConfig{}
inner := &wrapper.FallbackTreatment
if inner.GlobalFallbackTreatment != nil && inner.GlobalFallbackTreatment.Treatment != nil {
out.GlobalFallbackTreatment = &dtos.FallbackTreatment{
Treatment: inner.GlobalFallbackTreatment.Treatment,
Config: inner.GlobalFallbackTreatment.Config,
}
}
if len(inner.ByFlagFallbackTreatment) > 0 {
out.ByFlagFallbackTreatment = make(map[string]dtos.FallbackTreatment)
for name, v := range inner.ByFlagFallbackTreatment {
if v.Treatment != nil {
out.ByFlagFallbackTreatment[name] = dtos.FallbackTreatment{
Treatment: v.Treatment,
Config: v.Config,
}
}
}
}
return &out, nil
}

func ReadConfig() (*Config, error) {
cfgFN := defaultConfigFN
if fromEnv := os.Getenv("SPLITD_CONF_FILE"); fromEnv != "" {
Expand Down
44 changes: 44 additions & 0 deletions splitio/conf/splitd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"github.com/splitio/splitd/splitio/link/transfer"
"github.com/splitio/splitd/splitio/sdk/conf"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

func TestConfig(t *testing.T) {
Expand All @@ -30,6 +32,7 @@ func TestConfig(t *testing.T) {
cfg = Config{}

assert.Nil(t, cfg.parse(dir+string(filepath.Separator)+"splitd.yaml.tpl"))
expected.SDK.FallbackTreatment = cfg.SDK.FallbackTreatment
assert.Equal(t, expected, cfg)

assert.Error(t, cfg.parse("someNonexistantFile"))
Expand Down Expand Up @@ -185,3 +188,44 @@ func TestDefaultConf(t *testing.T) {
assert.Equal(t, defaultLogLevel, *c.Logger.Level)
assert.Equal(t, defaultLogOutput, *c.Logger.Output)
}

func TestFallbackTreatmentToSDKConf(t *testing.T) {
// JSON string form
var cfg Config
cfg.PopulateWithDefaults()
err := yaml.Unmarshal([]byte(`
sdk:
apikey: test
fallbackTreatment: '{"fallback_treatment":{"global_fallback_treatment":{"treatment":"control"},"by_flag_fallback_treatment":{"my_flag":{"treatment":"off"}}}}'
`), &cfg)
assert.Nil(t, err)
sdkConf := cfg.SDK.ToSDKConf()
require.NotNil(t, sdkConf)
require.NotNil(t, sdkConf.FallbackTreatment.GlobalFallbackTreatment)
require.NotEmpty(t, sdkConf.FallbackTreatment.ByFlagFallbackTreatment)
assert.Equal(t, "control", *sdkConf.FallbackTreatment.GlobalFallbackTreatment.Treatment)
assert.Equal(t, "off", *sdkConf.FallbackTreatment.ByFlagFallbackTreatment["my_flag"].Treatment)

// Native YAML object form
var cfg2 Config
cfg2.PopulateWithDefaults()
err = yaml.Unmarshal([]byte(`
sdk:
apikey: test
fallbackTreatment:
global_fallback_treatment:
treatment: global_val
by_flag_fallback_treatment:
some_flag:
treatment: on
config: "{}"
`), &cfg2)
assert.Nil(t, err)
sdkConf2 := cfg2.SDK.ToSDKConf()
require.NotNil(t, sdkConf2)
require.NotNil(t, sdkConf2.FallbackTreatment.GlobalFallbackTreatment)
require.Contains(t, sdkConf2.FallbackTreatment.ByFlagFallbackTreatment, "some_flag")
assert.Equal(t, "global_val", *sdkConf2.FallbackTreatment.GlobalFallbackTreatment.Treatment)
assert.Equal(t, "on", *sdkConf2.FallbackTreatment.ByFlagFallbackTreatment["some_flag"].Treatment)
assert.Equal(t, "{}", *sdkConf2.FallbackTreatment.ByFlagFallbackTreatment["some_flag"].Config)
}
14 changes: 10 additions & 4 deletions splitio/link/client/types/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import (
)

type ClientInterface interface {
Treatment(key string, bucketingKey string, feature string, attrs map[string]interface{}) (*Result, error)
Treatments(key string, bucketingKey string, features []string, attrs map[string]interface{}) (Results, error)
TreatmentWithConfig(key string, bucketingKey string, feature string, attrs map[string]interface{}) (*Result, error)
TreatmentsWithConfig(key string, bucketingKey string, features []string, attrs map[string]interface{}) (Results, error)
Treatment(key string, bucketingKey string, feature string, attrs map[string]interface{}, optFns ...OptFn) (*Result, error)
Treatments(key string, bucketingKey string, features []string, attrs map[string]interface{}, optFns ...OptFn) (Results, error)
TreatmentWithConfig(key string, bucketingKey string, feature string, attrs map[string]interface{}, optFns ...OptFn) (*Result, error)
TreatmentsWithConfig(key string, bucketingKey string, features []string, attrs map[string]interface{}, optFns ...OptFn) (Results, error)
Track(key string, trafficType string, eventType string, value *float64, properties map[string]interface{}) error
SplitNames() ([]string, error)
Split(name string) (*sdk.SplitView, error)
Expand All @@ -24,3 +24,9 @@ type Result struct {
}

type Results = map[string]Result

type Options struct {
EvaluationOptions *dtos.EvaluationOptions
}

type OptFn = func(o *Options)
Loading