Skip to content

[FSSDK-11589] Add go-sdk logic to support agent for cmab #412

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 58 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
54101be
Fix CMAB error handling to properly propagate error reasons in Decisi…
Mat001 Jun 27, 2025
d0c090a
add go-sdk logic to support agent for cmab
Mat001 Jun 27, 2025
40aef5e
cleanup debug statements
Mat001 Jun 27, 2025
6c419fe
add cmab cache options to getAllOptions
Mat001 Jun 27, 2025
5dcef46
fix failing fsc tests
Mat001 Jun 27, 2025
1ba0e3c
add cmab errors file
Mat001 Jun 27, 2025
c0ac22c
adjust lowercase
Mat001 Jun 27, 2025
c8b55e0
add test
Mat001 Jun 28, 2025
8305a90
fix error message propagation in resons
Mat001 Jul 15, 2025
45d51cf
add error handling to feature experiment servvice
Mat001 Jul 15, 2025
1e92f00
Add more error handling to feature exper and composite feature service
Mat001 Jul 15, 2025
7bcfe8a
nil back to err
Mat001 Jul 15, 2025
d2181fb
add reasons message to composite feature service GetDecision
Mat001 Jul 15, 2025
a76accc
use AddError for reasons
Mat001 Jul 15, 2025
2c913ba
Trigger PR check
Mat001 Jul 16, 2025
9d57add
merge in mpirnovar-fsc-failures-fix-fssdk-11649
Mat001 Jul 17, 2025
a1f4b66
fix cyclomatic complexity by refactoring client.go code
Mat001 Jul 17, 2025
7b76378
fix lint error
Mat001 Jul 17, 2025
6a8bae4
fix lint
Mat001 Jul 17, 2025
bfe1626
Trigger PR check
Mat001 Jul 16, 2025
3221bab
remove implicit error handling - PR feedback
Mat001 Jul 22, 2025
ac9d34b
[FSSDK-11649] Fix FSC failed tests for CMAB (#411)
Mat001 Jul 23, 2025
44be7aa
add go-sdk logic to support agent for cmab
Mat001 Jun 27, 2025
c64e543
fix failing fsc tests
Mat001 Jun 27, 2025
6b5ec11
adjust lowercase
Mat001 Jun 27, 2025
d959b1a
fix error message propagation in resons
Mat001 Jul 15, 2025
c14e466
add error handling to feature experiment servvice
Mat001 Jul 15, 2025
20967ec
Add more error handling to feature exper and composite feature service
Mat001 Jul 15, 2025
3f513c8
Trigger PR check
Mat001 Jul 16, 2025
7b66d8e
fix cyclomatic complexity by refactoring client.go code
Mat001 Jul 17, 2025
344a438
Trigger PR check
Mat001 Jul 16, 2025
068b036
remove implicit error handling - PR feedback
Mat001 Jul 22, 2025
fe88f6a
Update license year
Mat001 Jul 23, 2025
5cba2f8
Force GitHub refresh
Mat001 Jul 23, 2025
3b26094
Merge branch 'master' into mpirnovar-cmab-gosdk-agent-fssdk-11589
Mat001 Jul 23, 2025
d99fa90
change nill to err in feat exper service
Mat001 Jul 23, 2025
e17908d
fix tests
Mat001 Jul 24, 2025
9e40a65
add two tests
Mat001 Jul 24, 2025
31e2782
Force GitHub refresh
Mat001 Jul 24, 2025
d7bcec5
Add tests to address coveralls
Mat001 Jul 24, 2025
06a47dd
add couple more tests
Mat001 Jul 25, 2025
2763ee9
few more tests
Mat001 Jul 25, 2025
d5d321c
add test for TrGetCmabDecision
Mat001 Jul 25, 2025
7caa2d0
fix formatting
Mat001 Jul 25, 2025
2baee7e
add optional CmabUUID field to OptimizelyDecision for CMAB support
Mat001 Jul 25, 2025
1ad729f
simplify cmab agent support in go-sdk
Mat001 Jul 28, 2025
3778039
Add CMAB config struct and constants
Mat001 Jul 28, 2025
dfbff72
fix tests
Mat001 Jul 28, 2025
41d25e2
cleanup
Mat001 Jul 28, 2025
5b1f9a8
format
Mat001 Jul 28, 2025
c3cbc03
add tests for coveralls
Mat001 Jul 28, 2025
1682033
add new etst and format
Mat001 Jul 30, 2025
dcf46f6
Add CMAB support with config merging and remove CmabUUID from public API
Mat001 Aug 6, 2025
314ea2e
fix low test coverage
Mat001 Aug 6, 2025
13b235c
fix PR comments
Mat001 Aug 7, 2025
9af7525
additional fixes to pr comments
Mat001 Aug 7, 2025
d2c69a2
Fix whitespace in experiment_bucketer_service_test.go to match master
Mat001 Aug 7, 2025
2999807
update pred endpoint back to preduction, remove inte
Mat001 Aug 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 39 additions & 26 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
linters-settings:
govet:
check-shadowing: true
enable:
- shadow
# golint removed in newer versions, but works in v1.54.2
golint:
min-confidence: 0
gocyclo:
min-complexity: 16
maligned:
suggest-new: true
# maligned removed - replaced by govet fieldalignment
dupl:
threshold: 100
goconst:
Expand All @@ -31,45 +32,57 @@ linters-settings:
linters:
disable-all: true
enable:
- megacheck
- golint
# Core linters
- govet
- unconvert
- megacheck
- structcheck
- gas
- gocyclo
- dupl
- misspell
- unparam
- varcheck
- deadcode
- typecheck
- ineffassign
- varcheck
- gofmt

# Static analysis
- staticcheck
- gosimple
- unused

# Security
- gosec

# Style and quality
- golint # deprecated but still works in v1.54.2
- stylecheck
#- gochecknoinits
- scopelint

# Code complexity
- gocyclo
- dupl
- gocritic
- golint
- nakedret
- gosimple
- prealloc
- maligned
- gofmt

# Performance
- prealloc
- unconvert

# Correctness
- unparam
- misspell

# Replacing deprecated linters
- exportloopref # replaces scopelint (works in v1.54.2)
# - fieldalignment # replaces maligned (in govet)
fast: false

# Enable additional checks
enable-all: false

run:
skip-dirs:
- vendor
concurrency: 4

issues:
exclude-dirs:
- vendor
exclude-rules:
- text: "weak cryptographic primitive"
linters:
- gosec
exclude-use-default: false

service:
golangci-lint-version: 1.17.x
golangci-lint-version: 1.54.x
5 changes: 3 additions & 2 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2019-2024, Optimizely, Inc. and contributors *
* Copyright 2019-2025, Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand Down Expand Up @@ -173,6 +173,7 @@ func (o *OptimizelyClient) decide(userContext *OptimizelyUserContext, key string
Attributes: userContext.GetUserAttributes(),
QualifiedSegments: userContext.GetQualifiedSegments(),
}

var variationKey string
var eventSent, flagEnabled bool
allOptions := o.getAllOptions(options)
Expand Down Expand Up @@ -469,7 +470,7 @@ func (o *OptimizelyClient) Activate(experimentKey string, userContext entities.U
}

// IsFeatureEnabled returns true if the feature is enabled for the given user. If the user is part of a feature test
// then an impression event will be queued up to be sent to the Optimizely log endpoint for results processing.
// then an impression event will be queued up to the Optimizely log endpoint for results processing.
func (o *OptimizelyClient) IsFeatureEnabled(featureKey string, userContext entities.UserContext) (result bool, err error) {

defer func() {
Expand Down
15 changes: 14 additions & 1 deletion pkg/client/factory.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/****************************************************************************
* Copyright 2019-2020,2022-2024 Optimizely, Inc. and contributors *
* Copyright 2019-2020,2022-2025 Optimizely, Inc. and contributors *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); *
* you may not use this file except in compliance with the License. *
Expand All @@ -22,6 +22,7 @@ import (
"errors"
"time"

"github.com/optimizely/go-sdk/v2/pkg/cmab"
"github.com/optimizely/go-sdk/v2/pkg/config"
"github.com/optimizely/go-sdk/v2/pkg/decide"
"github.com/optimizely/go-sdk/v2/pkg/decision"
Expand Down Expand Up @@ -53,6 +54,7 @@ type OptimizelyFactory struct {
overrideStore decision.ExperimentOverrideStore
userProfileService decision.UserProfileService
notificationCenter notification.Center
cmabConfig *cmab.Config

// ODP
segmentsCacheSize int
Expand Down Expand Up @@ -159,6 +161,10 @@ func (f *OptimizelyFactory) Client(clientOptions ...OptionFunc) (*OptimizelyClie
if f.overrideStore != nil {
experimentServiceOptions = append(experimentServiceOptions, decision.WithOverrideStore(f.overrideStore))
}
// Add CMAB config option if provided
if f.cmabConfig != nil {
experimentServiceOptions = append(experimentServiceOptions, decision.WithCmabConfig(f.cmabConfig))
}
compositeExperimentService := decision.NewCompositeExperimentService(f.SDKKey, experimentServiceOptions...)
compositeService := decision.NewCompositeService(f.SDKKey, decision.WithCompositeExperimentService(compositeExperimentService))
appClient.DecisionService = compositeService
Expand Down Expand Up @@ -320,6 +326,13 @@ func WithTracer(tracer tracing.Tracer) OptionFunc {
}
}

// WithCmabConfig sets the CMAB configuration options
func WithCmabConfig(cmabConfig *cmab.Config) OptionFunc {
return func(f *OptimizelyFactory) {
f.cmabConfig = cmabConfig
}
}

// StaticClient returns a client initialized with a static project config.
func (f *OptimizelyFactory) StaticClient() (optlyClient *OptimizelyClient, err error) {

Expand Down
139 changes: 139 additions & 0 deletions pkg/client/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/stretchr/testify/mock"

"github.com/optimizely/go-sdk/v2/pkg/cache"
"github.com/optimizely/go-sdk/v2/pkg/cmab"
"github.com/optimizely/go-sdk/v2/pkg/config"
"github.com/optimizely/go-sdk/v2/pkg/decide"
"github.com/optimizely/go-sdk/v2/pkg/decision"
Expand Down Expand Up @@ -434,3 +435,141 @@ func TestConvertDecideOptionsWithCMABOptions(t *testing.T) {
assert.True(t, convertedOptions.ResetCMABCache)
assert.True(t, convertedOptions.InvalidateUserCMABCache)
}

func TestAllOptionFunctions(t *testing.T) {
f := &OptimizelyFactory{}

// Test all option functions to ensure they're covered
WithDatafileAccessToken("token")(f)
WithSegmentsCacheSize(123)(f)
WithSegmentsCacheTimeout(2 * time.Second)(f)
WithOdpDisabled(true)(f)

// Verify some options were set
assert.Equal(t, "token", f.DatafileAccessToken)
assert.Equal(t, 123, f.segmentsCacheSize)
assert.True(t, f.odpDisabled)
}

func TestStaticClientError(t *testing.T) {
// Use invalid datafile to force an error
factory := OptimizelyFactory{Datafile: []byte("invalid json"), SDKKey: ""}
client, err := factory.StaticClient()
assert.Error(t, err)
assert.Nil(t, client)
}

func TestFactoryWithCmabConfig(t *testing.T) {
factory := OptimizelyFactory{}
cmabConfig := cmab.Config{
CacheSize: 100,
CacheTTL: time.Minute,
HTTPTimeout: 30 * time.Second,
RetryConfig: &cmab.RetryConfig{
MaxRetries: 5,
},
}

// Test the option function
WithCmabConfig(&cmabConfig)(&factory)

assert.Equal(t, &cmabConfig, factory.cmabConfig)
assert.Equal(t, 100, factory.cmabConfig.CacheSize)
assert.Equal(t, time.Minute, factory.cmabConfig.CacheTTL)
assert.Equal(t, 30*time.Second, factory.cmabConfig.HTTPTimeout)
assert.NotNil(t, factory.cmabConfig.RetryConfig)
assert.Equal(t, 5, factory.cmabConfig.RetryConfig.MaxRetries)
}

func TestFactoryCmabConfigPassedToDecisionService(t *testing.T) {
// Test that CMAB config is correctly passed to decision service when creating client
cmabConfig := cmab.Config{
CacheSize: 200,
CacheTTL: 2 * time.Minute,
HTTPTimeout: 20 * time.Second,
RetryConfig: &cmab.RetryConfig{
MaxRetries: 3,
},
}

factory := OptimizelyFactory{
SDKKey: "test_sdk_key",
cmabConfig: &cmabConfig,
}

// Verify the config is set
assert.Equal(t, &cmabConfig, factory.cmabConfig)
assert.Equal(t, 200, factory.cmabConfig.CacheSize)
assert.Equal(t, 2*time.Minute, factory.cmabConfig.CacheTTL)
assert.NotNil(t, factory.cmabConfig.RetryConfig)
}

func TestFactoryOptionFunctions(t *testing.T) {
factory := &OptimizelyFactory{}

// Test all option functions to ensure they're covered
WithDatafileAccessToken("test_token")(factory)
WithSegmentsCacheSize(100)(factory)
WithSegmentsCacheTimeout(5 * time.Second)(factory)
WithOdpDisabled(true)(factory)
WithCmabConfig(&cmab.Config{CacheSize: 50})(factory)

// Verify options were set
assert.Equal(t, "test_token", factory.DatafileAccessToken)
assert.Equal(t, 100, factory.segmentsCacheSize)
assert.Equal(t, 5*time.Second, factory.segmentsCacheTimeout)
assert.True(t, factory.odpDisabled)
assert.Equal(t, &cmab.Config{CacheSize: 50}, factory.cmabConfig)
}

func TestWithCmabConfigOption(t *testing.T) {
factory := &OptimizelyFactory{}
testConfig := cmab.Config{
CacheSize: 200,
CacheTTL: 2 * time.Minute,
}
WithCmabConfig(&testConfig)(factory)
assert.Equal(t, &testConfig, factory.cmabConfig)
}

func TestClientWithCmabConfig(t *testing.T) {
// Test client creation with non-empty CMAB config (tests reflect.DeepEqual path)
cmabConfig := cmab.Config{
CacheSize: 200,
CacheTTL: 5 * time.Minute,
HTTPTimeout: 30 * time.Second,
RetryConfig: &cmab.RetryConfig{
MaxRetries: 5,
},
}

factory := OptimizelyFactory{
SDKKey: "test_sdk_key",
}

client, err := factory.Client(WithCmabConfig(&cmabConfig))
assert.NoError(t, err)
assert.NotNil(t, client)

// Verify the CMAB config was applied by checking if DecisionService exists
// This tests the reflect.DeepEqual check on lines 166-167
assert.NotNil(t, client.DecisionService)
client.Close()
}

func TestClientWithEmptyCmabConfig(t *testing.T) {
// Test client creation with empty CMAB config (tests reflect.DeepEqual returns true)
emptyCmabConfig := cmab.Config{}

factory := OptimizelyFactory{
SDKKey: "test_sdk_key",
}

client, err := factory.Client(WithCmabConfig(&emptyCmabConfig))
assert.NoError(t, err)
assert.NotNil(t, client)

// Verify client still works with empty config
assert.NotNil(t, client.DecisionService)
client.Close()
}
8 changes: 7 additions & 1 deletion pkg/client/optimizely_decision.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ type OptimizelyDecision struct {
}

// NewOptimizelyDecision creates and returns a new instance of OptimizelyDecision
func NewOptimizelyDecision(variationKey, ruleKey, flagKey string, enabled bool, variables *optimizelyjson.OptimizelyJSON, userContext OptimizelyUserContext, reasons []string) OptimizelyDecision {
func NewOptimizelyDecision(
variationKey, ruleKey, flagKey string,
enabled bool,
variables *optimizelyjson.OptimizelyJSON,
userContext OptimizelyUserContext,
reasons []string,
) OptimizelyDecision {
return OptimizelyDecision{
VariationKey: variationKey,
Enabled: enabled,
Expand Down
Loading