Skip to content

Commit e9f9fa7

Browse files
authored
[FSSDK-11589] Add go-sdk logic to support agent for cmab (#412)
* Fix CMAB error handling to properly propagate error reasons in Decision objects * add go-sdk logic to support agent for cmab * cleanup debug statements * add cmab cache options to getAllOptions * fix failing fsc tests * add cmab errors file * adjust lowercase * add test * fix error message propagation in resons * add error handling to feature experiment servvice * Add more error handling to feature exper and composite feature service * nil back to err * add reasons message to composite feature service GetDecision * use AddError for reasons * Trigger PR check * fix cyclomatic complexity by refactoring client.go code * fix lint error * fix lint * Trigger PR check * remove implicit error handling - PR feedback * [FSSDK-11649] Fix FSC failed tests for CMAB (#411) * Fix CMAB error handling to properly propagate error reasons in Decision objects * add cmab cache options to getAllOptions * fix failing fsc tests * add cmab errors file * adjust lowercase * add test * fix error message propagation in resons * add error handling to feature experiment servvice * Add more error handling to feature exper and composite feature service * nil back to err * add reasons message to composite feature service GetDecision * use AddError for reasons * Trigger PR check * remove implicit error handling - PR feedback * use nil instead of err for legacy * fix error format * Fix lint issue with fsc error * Rename error var, lint stuttering issue * add go-sdk logic to support agent for cmab * fix failing fsc tests * adjust lowercase * fix error message propagation in resons * add error handling to feature experiment servvice * Add more error handling to feature exper and composite feature service * Trigger PR check * fix cyclomatic complexity by refactoring client.go code * Trigger PR check * remove implicit error handling - PR feedback * Update license year * Force GitHub refresh * change nill to err in feat exper service * fix tests * add two tests * Force GitHub refresh * Add tests to address coveralls * add couple more tests * few more tests * add test for TrGetCmabDecision * fix formatting * add optional CmabUUID field to OptimizelyDecision for CMAB support * simplify cmab agent support in go-sdk * Add CMAB config struct and constants * fix tests * cleanup * format * add tests for coveralls * add new etst and format * Add CMAB support with config merging and remove CmabUUID from public API * fix low test coverage * fix PR comments * additional fixes to pr comments * Fix whitespace in experiment_bucketer_service_test.go to match master - Restore correct space indentation to match master branch - Addresses coworker comment: 'can we keep this file unchanged?' * update pred endpoint back to preduction, remove inte
1 parent afaf5d6 commit e9f9fa7

16 files changed

+545
-80
lines changed

.golangci.yml

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
linters-settings:
22
govet:
3-
check-shadowing: true
3+
enable:
4+
- shadow
5+
# golint removed in newer versions, but works in v1.54.2
46
golint:
57
min-confidence: 0
68
gocyclo:
79
min-complexity: 16
8-
maligned:
9-
suggest-new: true
10+
# maligned removed - replaced by govet fieldalignment
1011
dupl:
1112
threshold: 100
1213
goconst:
@@ -31,45 +32,57 @@ linters-settings:
3132
linters:
3233
disable-all: true
3334
enable:
34-
- megacheck
35-
- golint
35+
# Core linters
3636
- govet
37-
- unconvert
38-
- megacheck
39-
- structcheck
40-
- gas
41-
- gocyclo
42-
- dupl
43-
- misspell
44-
- unparam
45-
- varcheck
46-
- deadcode
4737
- typecheck
4838
- ineffassign
49-
- varcheck
39+
- gofmt
40+
41+
# Static analysis
42+
- staticcheck
43+
- gosimple
44+
- unused
45+
46+
# Security
47+
- gosec
48+
49+
# Style and quality
50+
- golint # deprecated but still works in v1.54.2
5051
- stylecheck
51-
#- gochecknoinits
52-
- scopelint
52+
53+
# Code complexity
54+
- gocyclo
55+
- dupl
5356
- gocritic
54-
- golint
5557
- nakedret
56-
- gosimple
57-
- prealloc
58-
- maligned
59-
- gofmt
58+
59+
# Performance
60+
- prealloc
61+
- unconvert
62+
63+
# Correctness
64+
- unparam
65+
- misspell
66+
67+
# Replacing deprecated linters
68+
- exportloopref # replaces scopelint (works in v1.54.2)
69+
# - fieldalignment # replaces maligned (in govet)
6070
fast: false
71+
72+
# Enable additional checks
73+
enable-all: false
6174

6275
run:
63-
skip-dirs:
64-
- vendor
6576
concurrency: 4
6677

6778
issues:
79+
exclude-dirs:
80+
- vendor
6881
exclude-rules:
6982
- text: "weak cryptographic primitive"
7083
linters:
7184
- gosec
7285
exclude-use-default: false
7386

7487
service:
75-
golangci-lint-version: 1.17.x
88+
golangci-lint-version: 1.54.x

pkg/client/client.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2019-2024, Optimizely, Inc. and contributors *
2+
* Copyright 2019-2025, Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -173,6 +173,7 @@ func (o *OptimizelyClient) decide(userContext *OptimizelyUserContext, key string
173173
Attributes: userContext.GetUserAttributes(),
174174
QualifiedSegments: userContext.GetQualifiedSegments(),
175175
}
176+
176177
var variationKey string
177178
var eventSent, flagEnabled bool
178179
allOptions := o.getAllOptions(options)
@@ -469,7 +470,7 @@ func (o *OptimizelyClient) Activate(experimentKey string, userContext entities.U
469470
}
470471

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

475476
defer func() {

pkg/client/factory.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/****************************************************************************
2-
* Copyright 2019-2020,2022-2024 Optimizely, Inc. and contributors *
2+
* Copyright 2019-2020,2022-2025 Optimizely, Inc. and contributors *
33
* *
44
* Licensed under the Apache License, Version 2.0 (the "License"); *
55
* you may not use this file except in compliance with the License. *
@@ -22,6 +22,7 @@ import (
2222
"errors"
2323
"time"
2424

25+
"github.com/optimizely/go-sdk/v2/pkg/cmab"
2526
"github.com/optimizely/go-sdk/v2/pkg/config"
2627
"github.com/optimizely/go-sdk/v2/pkg/decide"
2728
"github.com/optimizely/go-sdk/v2/pkg/decision"
@@ -53,6 +54,7 @@ type OptimizelyFactory struct {
5354
overrideStore decision.ExperimentOverrideStore
5455
userProfileService decision.UserProfileService
5556
notificationCenter notification.Center
57+
cmabConfig *cmab.Config
5658

5759
// ODP
5860
segmentsCacheSize int
@@ -159,6 +161,10 @@ func (f *OptimizelyFactory) Client(clientOptions ...OptionFunc) (*OptimizelyClie
159161
if f.overrideStore != nil {
160162
experimentServiceOptions = append(experimentServiceOptions, decision.WithOverrideStore(f.overrideStore))
161163
}
164+
// Add CMAB config option if provided
165+
if f.cmabConfig != nil {
166+
experimentServiceOptions = append(experimentServiceOptions, decision.WithCmabConfig(f.cmabConfig))
167+
}
162168
compositeExperimentService := decision.NewCompositeExperimentService(f.SDKKey, experimentServiceOptions...)
163169
compositeService := decision.NewCompositeService(f.SDKKey, decision.WithCompositeExperimentService(compositeExperimentService))
164170
appClient.DecisionService = compositeService
@@ -320,6 +326,13 @@ func WithTracer(tracer tracing.Tracer) OptionFunc {
320326
}
321327
}
322328

329+
// WithCmabConfig sets the CMAB configuration options
330+
func WithCmabConfig(cmabConfig *cmab.Config) OptionFunc {
331+
return func(f *OptimizelyFactory) {
332+
f.cmabConfig = cmabConfig
333+
}
334+
}
335+
323336
// StaticClient returns a client initialized with a static project config.
324337
func (f *OptimizelyFactory) StaticClient() (optlyClient *OptimizelyClient, err error) {
325338

pkg/client/factory_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/stretchr/testify/mock"
2929

3030
"github.com/optimizely/go-sdk/v2/pkg/cache"
31+
"github.com/optimizely/go-sdk/v2/pkg/cmab"
3132
"github.com/optimizely/go-sdk/v2/pkg/config"
3233
"github.com/optimizely/go-sdk/v2/pkg/decide"
3334
"github.com/optimizely/go-sdk/v2/pkg/decision"
@@ -434,3 +435,141 @@ func TestConvertDecideOptionsWithCMABOptions(t *testing.T) {
434435
assert.True(t, convertedOptions.ResetCMABCache)
435436
assert.True(t, convertedOptions.InvalidateUserCMABCache)
436437
}
438+
439+
func TestAllOptionFunctions(t *testing.T) {
440+
f := &OptimizelyFactory{}
441+
442+
// Test all option functions to ensure they're covered
443+
WithDatafileAccessToken("token")(f)
444+
WithSegmentsCacheSize(123)(f)
445+
WithSegmentsCacheTimeout(2 * time.Second)(f)
446+
WithOdpDisabled(true)(f)
447+
448+
// Verify some options were set
449+
assert.Equal(t, "token", f.DatafileAccessToken)
450+
assert.Equal(t, 123, f.segmentsCacheSize)
451+
assert.True(t, f.odpDisabled)
452+
}
453+
454+
func TestStaticClientError(t *testing.T) {
455+
// Use invalid datafile to force an error
456+
factory := OptimizelyFactory{Datafile: []byte("invalid json"), SDKKey: ""}
457+
client, err := factory.StaticClient()
458+
assert.Error(t, err)
459+
assert.Nil(t, client)
460+
}
461+
462+
func TestFactoryWithCmabConfig(t *testing.T) {
463+
factory := OptimizelyFactory{}
464+
cmabConfig := cmab.Config{
465+
CacheSize: 100,
466+
CacheTTL: time.Minute,
467+
HTTPTimeout: 30 * time.Second,
468+
RetryConfig: &cmab.RetryConfig{
469+
MaxRetries: 5,
470+
},
471+
}
472+
473+
// Test the option function
474+
WithCmabConfig(&cmabConfig)(&factory)
475+
476+
assert.Equal(t, &cmabConfig, factory.cmabConfig)
477+
assert.Equal(t, 100, factory.cmabConfig.CacheSize)
478+
assert.Equal(t, time.Minute, factory.cmabConfig.CacheTTL)
479+
assert.Equal(t, 30*time.Second, factory.cmabConfig.HTTPTimeout)
480+
assert.NotNil(t, factory.cmabConfig.RetryConfig)
481+
assert.Equal(t, 5, factory.cmabConfig.RetryConfig.MaxRetries)
482+
}
483+
484+
func TestFactoryCmabConfigPassedToDecisionService(t *testing.T) {
485+
// Test that CMAB config is correctly passed to decision service when creating client
486+
cmabConfig := cmab.Config{
487+
CacheSize: 200,
488+
CacheTTL: 2 * time.Minute,
489+
HTTPTimeout: 20 * time.Second,
490+
RetryConfig: &cmab.RetryConfig{
491+
MaxRetries: 3,
492+
},
493+
}
494+
495+
factory := OptimizelyFactory{
496+
SDKKey: "test_sdk_key",
497+
cmabConfig: &cmabConfig,
498+
}
499+
500+
// Verify the config is set
501+
assert.Equal(t, &cmabConfig, factory.cmabConfig)
502+
assert.Equal(t, 200, factory.cmabConfig.CacheSize)
503+
assert.Equal(t, 2*time.Minute, factory.cmabConfig.CacheTTL)
504+
assert.NotNil(t, factory.cmabConfig.RetryConfig)
505+
}
506+
507+
func TestFactoryOptionFunctions(t *testing.T) {
508+
factory := &OptimizelyFactory{}
509+
510+
// Test all option functions to ensure they're covered
511+
WithDatafileAccessToken("test_token")(factory)
512+
WithSegmentsCacheSize(100)(factory)
513+
WithSegmentsCacheTimeout(5 * time.Second)(factory)
514+
WithOdpDisabled(true)(factory)
515+
WithCmabConfig(&cmab.Config{CacheSize: 50})(factory)
516+
517+
// Verify options were set
518+
assert.Equal(t, "test_token", factory.DatafileAccessToken)
519+
assert.Equal(t, 100, factory.segmentsCacheSize)
520+
assert.Equal(t, 5*time.Second, factory.segmentsCacheTimeout)
521+
assert.True(t, factory.odpDisabled)
522+
assert.Equal(t, &cmab.Config{CacheSize: 50}, factory.cmabConfig)
523+
}
524+
525+
func TestWithCmabConfigOption(t *testing.T) {
526+
factory := &OptimizelyFactory{}
527+
testConfig := cmab.Config{
528+
CacheSize: 200,
529+
CacheTTL: 2 * time.Minute,
530+
}
531+
WithCmabConfig(&testConfig)(factory)
532+
assert.Equal(t, &testConfig, factory.cmabConfig)
533+
}
534+
535+
func TestClientWithCmabConfig(t *testing.T) {
536+
// Test client creation with non-empty CMAB config (tests reflect.DeepEqual path)
537+
cmabConfig := cmab.Config{
538+
CacheSize: 200,
539+
CacheTTL: 5 * time.Minute,
540+
HTTPTimeout: 30 * time.Second,
541+
RetryConfig: &cmab.RetryConfig{
542+
MaxRetries: 5,
543+
},
544+
}
545+
546+
factory := OptimizelyFactory{
547+
SDKKey: "test_sdk_key",
548+
}
549+
550+
client, err := factory.Client(WithCmabConfig(&cmabConfig))
551+
assert.NoError(t, err)
552+
assert.NotNil(t, client)
553+
554+
// Verify the CMAB config was applied by checking if DecisionService exists
555+
// This tests the reflect.DeepEqual check on lines 166-167
556+
assert.NotNil(t, client.DecisionService)
557+
client.Close()
558+
}
559+
560+
func TestClientWithEmptyCmabConfig(t *testing.T) {
561+
// Test client creation with empty CMAB config (tests reflect.DeepEqual returns true)
562+
emptyCmabConfig := cmab.Config{}
563+
564+
factory := OptimizelyFactory{
565+
SDKKey: "test_sdk_key",
566+
}
567+
568+
client, err := factory.Client(WithCmabConfig(&emptyCmabConfig))
569+
assert.NoError(t, err)
570+
assert.NotNil(t, client)
571+
572+
// Verify client still works with empty config
573+
assert.NotNil(t, client.DecisionService)
574+
client.Close()
575+
}

pkg/client/optimizely_decision.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@ type OptimizelyDecision struct {
3333
}
3434

3535
// NewOptimizelyDecision creates and returns a new instance of OptimizelyDecision
36-
func NewOptimizelyDecision(variationKey, ruleKey, flagKey string, enabled bool, variables *optimizelyjson.OptimizelyJSON, userContext OptimizelyUserContext, reasons []string) OptimizelyDecision {
36+
func NewOptimizelyDecision(
37+
variationKey, ruleKey, flagKey string,
38+
enabled bool,
39+
variables *optimizelyjson.OptimizelyJSON,
40+
userContext OptimizelyUserContext,
41+
reasons []string,
42+
) OptimizelyDecision {
3743
return OptimizelyDecision{
3844
VariationKey: variationKey,
3945
Enabled: enabled,

0 commit comments

Comments
 (0)