diff --git a/eppoclient/assignmentlogger.go b/eppoclient/assignmentlogger.go index 0e35fb1..d58dfec 100644 --- a/eppoclient/assignmentlogger.go +++ b/eppoclient/assignmentlogger.go @@ -1,11 +1,20 @@ package eppoclient -import "fmt" +import ( + "context" + "fmt" +) +// IAssignmentLogger is be deprecated at next major version +// and replaced by IAssignmentLoggerContext type IAssignmentLogger interface { LogAssignment(event AssignmentEvent) } +type IAssignmentLoggerContext interface { + LogAssignment(context.Context, AssignmentEvent) +} + // BanditActionLogger is going to be merged into IAssignmentLogger in // the next major version. type BanditActionLogger interface { diff --git a/eppoclient/bandits_test.go b/eppoclient/bandits_test.go index 237e474..a20cf9d 100644 --- a/eppoclient/bandits_test.go +++ b/eppoclient/bandits_test.go @@ -61,7 +61,7 @@ func Test_bandits_sdkTestData(t *testing.T) { logger := new(mockLogger) logger.Mock.On("LogAssignment", mock.Anything).Return() logger.Mock.On("LogBanditAction", mock.Anything).Return() - client := newEppoClient(configStore, nil, nil, logger, applicationLogger) + client := newEppoClient(configStore, nil, nil, logger, nil, applicationLogger) tests := readJsonDirectory[banditTest]("test-data/ufc/bandit-tests/") for file, test := range tests { diff --git a/eppoclient/client.go b/eppoclient/client.go index 030ebe8..03ba5c9 100644 --- a/eppoclient/client.go +++ b/eppoclient/client.go @@ -1,6 +1,7 @@ package eppoclient import ( + "context" "fmt" "time" ) @@ -14,6 +15,7 @@ type EppoClient struct { configRequestor *configurationRequestor poller *poller logger IAssignmentLogger + loggerContext IAssignmentLoggerContext applicationLogger ApplicationLogger } @@ -22,6 +24,7 @@ func newEppoClient( configRequestor *configurationRequestor, poller *poller, assignmentLogger IAssignmentLogger, + assignmentLoggerContext IAssignmentLoggerContext, applicationLogger ApplicationLogger, ) *EppoClient { return &EppoClient{ @@ -29,6 +32,7 @@ func newEppoClient( configRequestor: configRequestor, poller: poller, logger: assignmentLogger, + loggerContext: assignmentLoggerContext, applicationLogger: applicationLogger, } } @@ -39,16 +43,38 @@ func newEppoClient( // It is recommended to apply a timeout to initialization as otherwise // it may hang up indefinitely. // -// select { -// case <-client.Initialized(): -// case <-time.After(5 * time.Second): -// } +// select { +// case <-client.Initialized(): +// case <-time.After(5 * time.Second): +// } func (ec *EppoClient) Initialized() <-chan struct{} { return ec.configurationStore.Initialized() } -func (ec *EppoClient) GetBoolAssignment(flagKey string, subjectKey string, subjectAttributes Attributes, defaultValue bool) (bool, error) { - variation, err := ec.getAssignment(ec.configurationStore.getConfiguration(), flagKey, subjectKey, subjectAttributes, booleanVariation) +func (ec *EppoClient) GetBoolAssignment( + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue bool, +) (bool, error) { + return ec.getBoolAssignment(context.Background(), flagKey, subjectKey, subjectAttributes, defaultValue) +} + +func (ec *EppoClient) GetBoolAssignmentContext( + ctx context.Context, + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue bool, +) (bool, error) { + return ec.getBoolAssignment(ctx, flagKey, subjectKey, subjectAttributes, defaultValue) +} + +func (ec *EppoClient) getBoolAssignment( + ctx context.Context, + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue bool, +) (bool, error) { + variation, err := ec.getAssignment(ctx, ec.configurationStore.getConfiguration(), flagKey, subjectKey, subjectAttributes, booleanVariation) if err != nil || variation == nil { return defaultValue, err } @@ -60,8 +86,30 @@ func (ec *EppoClient) GetBoolAssignment(flagKey string, subjectKey string, subje return result, err } -func (ec *EppoClient) GetNumericAssignment(flagKey string, subjectKey string, subjectAttributes Attributes, defaultValue float64) (float64, error) { - variation, err := ec.getAssignment(ec.configurationStore.getConfiguration(), flagKey, subjectKey, subjectAttributes, numericVariation) +func (ec *EppoClient) GetNumericAssignment( + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue float64, +) (float64, error) { + return ec.getNumericAssignment(context.Background(), flagKey, subjectKey, subjectAttributes, defaultValue) +} + +func (ec *EppoClient) GetNumericAssignmentContext( + ctx context.Context, + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue float64, +) (float64, error) { + return ec.getNumericAssignment(ctx, flagKey, subjectKey, subjectAttributes, defaultValue) +} + +func (ec *EppoClient) getNumericAssignment( + ctx context.Context, + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue float64, +) (float64, error) { + variation, err := ec.getAssignment(ctx, ec.configurationStore.getConfiguration(), flagKey, subjectKey, subjectAttributes, numericVariation) if err != nil || variation == nil { return defaultValue, err } @@ -73,8 +121,30 @@ func (ec *EppoClient) GetNumericAssignment(flagKey string, subjectKey string, su return result, err } -func (ec *EppoClient) GetIntegerAssignment(flagKey string, subjectKey string, subjectAttributes Attributes, defaultValue int64) (int64, error) { - variation, err := ec.getAssignment(ec.configurationStore.getConfiguration(), flagKey, subjectKey, subjectAttributes, integerVariation) +func (ec *EppoClient) GetIntegerAssignment( + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue int64, +) (int64, error) { + return ec.getIntegerAssignment(context.Background(), flagKey, subjectKey, subjectAttributes, defaultValue) +} + +func (ec *EppoClient) GetIntegerAssignmentContext( + ctx context.Context, + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue int64, +) (int64, error) { + return ec.getIntegerAssignment(context.Background(), flagKey, subjectKey, subjectAttributes, defaultValue) +} + +func (ec *EppoClient) getIntegerAssignment( + ctx context.Context, + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue int64, +) (int64, error) { + variation, err := ec.getAssignment(ctx, ec.configurationStore.getConfiguration(), flagKey, subjectKey, subjectAttributes, integerVariation) if err != nil || variation == nil { return defaultValue, err } @@ -86,8 +156,30 @@ func (ec *EppoClient) GetIntegerAssignment(flagKey string, subjectKey string, su return result, err } -func (ec *EppoClient) GetStringAssignment(flagKey string, subjectKey string, subjectAttributes Attributes, defaultValue string) (string, error) { - variation, err := ec.getAssignment(ec.configurationStore.getConfiguration(), flagKey, subjectKey, subjectAttributes, stringVariation) +func (ec *EppoClient) GetStringAssignment( + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue string, +) (string, error) { + return ec.getStringAssignment(context.Background(), flagKey, subjectKey, subjectAttributes, defaultValue) +} + +func (ec *EppoClient) GetStringAssignmentContext( + ctx context.Context, + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue string, +) (string, error) { + return ec.getStringAssignment(ctx, flagKey, subjectKey, subjectAttributes, defaultValue) +} + +func (ec *EppoClient) getStringAssignment( + ctx context.Context, + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue string, +) (string, error) { + variation, err := ec.getAssignment(ctx, ec.configurationStore.getConfiguration(), flagKey, subjectKey, subjectAttributes, stringVariation) if err != nil || variation == nil { return defaultValue, err } @@ -99,8 +191,30 @@ func (ec *EppoClient) GetStringAssignment(flagKey string, subjectKey string, sub return result, err } -func (ec *EppoClient) GetJSONAssignment(flagKey string, subjectKey string, subjectAttributes Attributes, defaultValue interface{}) (interface{}, error) { - variation, err := ec.getAssignment(ec.configurationStore.getConfiguration(), flagKey, subjectKey, subjectAttributes, jsonVariation) +func (ec *EppoClient) GetJSONAssignment( + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue any, +) (any, error) { + return ec.getJSONAssignment(context.Background(), flagKey, subjectKey, subjectAttributes, defaultValue) +} + +func (ec *EppoClient) GetJSONAssignmentContext( + ctx context.Context, + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue any, +) (any, error) { + return ec.getJSONAssignment(ctx, flagKey, subjectKey, subjectAttributes, defaultValue) +} + +func (ec *EppoClient) getJSONAssignment( + ctx context.Context, + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue any, +) (any, error) { + variation, err := ec.getAssignment(ctx, ec.configurationStore.getConfiguration(), flagKey, subjectKey, subjectAttributes, jsonVariation) if err != nil || variation == nil { return defaultValue, err } @@ -112,8 +226,30 @@ func (ec *EppoClient) GetJSONAssignment(flagKey string, subjectKey string, subje return result.Parsed, err } -func (ec *EppoClient) GetJSONBytesAssignment(flagKey string, subjectKey string, subjectAttributes Attributes, defaultValue []byte) ([]byte, error) { - variation, err := ec.getAssignment(ec.configurationStore.getConfiguration(), flagKey, subjectKey, subjectAttributes, jsonVariation) +func (ec *EppoClient) GetJSONBytesAssignment( + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue []byte, +) ([]byte, error) { + return ec.getJSONBytesAssignment(context.Background(), flagKey, subjectKey, subjectAttributes, defaultValue) +} + +func (ec *EppoClient) GetJSONBytesAssignmentContext( + ctx context.Context, + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue []byte, +) ([]byte, error) { + return ec.getJSONBytesAssignment(ctx, flagKey, subjectKey, subjectAttributes, defaultValue) +} + +func (ec *EppoClient) getJSONBytesAssignment( + ctx context.Context, + flagKey, subjectKey string, + subjectAttributes Attributes, + defaultValue []byte, +) ([]byte, error) { + variation, err := ec.getAssignment(ctx, ec.configurationStore.getConfiguration(), flagKey, subjectKey, subjectAttributes, jsonVariation) if err != nil || variation == nil { return defaultValue, err } @@ -130,11 +266,36 @@ type BanditResult struct { Action *string } -func (ec *EppoClient) GetBanditAction(flagKey string, subjectKey string, subjectAttributes ContextAttributes, actions map[string]ContextAttributes, defaultVariation string) BanditResult { +func (ec *EppoClient) GetBanditAction( + flagKey, subjectKey string, + subjectAttributes ContextAttributes, + actions map[string]ContextAttributes, + defaultVariation string, +) BanditResult { + return ec.getBanditAction(context.Background(), flagKey, subjectKey, subjectAttributes, actions, defaultVariation) +} + +func (ec *EppoClient) GetBanditActionContext( + ctx context.Context, + flagKey, subjectKey string, + subjectAttributes ContextAttributes, + actions map[string]ContextAttributes, + defaultVariation string, +) BanditResult { + return ec.getBanditAction(ctx, flagKey, subjectKey, subjectAttributes, actions, defaultVariation) +} + +func (ec *EppoClient) getBanditAction( + ctx context.Context, + flagKey, subjectKey string, + subjectAttributes ContextAttributes, + actions map[string]ContextAttributes, + defaultVariation string, +) BanditResult { config := ec.configurationStore.getConfiguration() // ignoring the error here as we can always proceed with default variation - assignmentValue, _ := ec.getAssignment(config, flagKey, subjectKey, subjectAttributes.toGenericAttributes(), stringVariation) + assignmentValue, _ := ec.getAssignment(ctx, config, flagKey, subjectKey, subjectAttributes.toGenericAttributes(), stringVariation) variation, ok := assignmentValue.(string) if !ok { variation = defaultVariation @@ -172,38 +333,24 @@ func (ec *EppoClient) GetBanditAction(flagKey string, subjectKey string, subject actions: actions, }) - if logger, ok := ec.logger.(BanditActionLogger); ok { - event := BanditEvent{ - FlagKey: flagKey, - BanditKey: bandit.BanditKey, - Subject: subjectKey, - Action: evaluation.actionKey, - ActionProbability: evaluation.actionWeight, - OptimalityGap: evaluation.optimalityGap, - ModelVersion: bandit.ModelVersion, - Timestamp: time.Now().UTC().Format(time.RFC3339), - SubjectNumericAttributes: evaluation.subjectAttributes.Numeric, - SubjectCategoricalAttributes: evaluation.subjectAttributes.Categorical, - ActionNumericAttributes: evaluation.actionAttributes.Numeric, - ActionCategoricalAttributes: evaluation.actionAttributes.Categorical, - MetaData: map[string]string{ - "sdkLanguage": "go", - "sdkVersion": __version__, - }, - } - - func() { - // need to catch panics from Logger and continue - defer func() { - r := recover() - if r != nil { - fmt.Println("panic occurred:", r) - } - }() - - logger.LogBanditAction(event) - }() - } + ec.logBanditAction(ctx, BanditEvent{ + FlagKey: flagKey, + BanditKey: bandit.BanditKey, + Subject: subjectKey, + Action: evaluation.actionKey, + ActionProbability: evaluation.actionWeight, + OptimalityGap: evaluation.optimalityGap, + ModelVersion: bandit.ModelVersion, + Timestamp: time.Now().UTC().Format(time.RFC3339), + SubjectNumericAttributes: evaluation.subjectAttributes.Numeric, + SubjectCategoricalAttributes: evaluation.subjectAttributes.Categorical, + ActionNumericAttributes: evaluation.actionAttributes.Numeric, + ActionCategoricalAttributes: evaluation.actionAttributes.Categorical, + MetaData: map[string]string{ + "sdkLanguage": "go", + "sdkVersion": __version__, + }, + }) return BanditResult{ Variation: variation, @@ -211,7 +358,7 @@ func (ec *EppoClient) GetBanditAction(flagKey string, subjectKey string, subject } } -func (ec *EppoClient) getAssignment(config configuration, flagKey string, subjectKey string, subjectAttributes Attributes, variationType variationType) (interface{}, error) { +func (ec *EppoClient) getAssignment(ctx context.Context, config configuration, flagKey string, subjectKey string, subjectAttributes Attributes, variationType variationType) (interface{}, error) { if subjectKey == "" { return nil, fmt.Errorf("no subject key provided") } @@ -238,20 +385,53 @@ func (ec *EppoClient) getAssignment(config configuration, flagKey string, subjec return nil, err } - if assignmentEvent != nil { - func() { - // need to catch panics from Logger and continue - defer func() { - r := recover() - if r != nil { - ec.applicationLogger.Errorf("panic occurred: %v", r) - } - }() - - // Log assignment - ec.logger.LogAssignment(*assignmentEvent) - }() + ec.logAssignment(ctx, assignmentEvent) + return assignmentValue, nil +} + +func (ec *EppoClient) logAssignment(ctx context.Context, event *AssignmentEvent) { + if event == nil { + return } - return assignmentValue, nil + // need to catch panics from Logger and continue + defer func() { + r := recover() + if r != nil { + ec.applicationLogger.Errorf("panic occurred: %v", r) + } + }() + + switch { + case ec.loggerContext != nil: + // prioritise logger context if it's defined in the config + ec.loggerContext.LogAssignment(ctx, *event) + case ec.logger != nil: + ec.logger.LogAssignment(*event) + default: + // no need to do anything if both logger are nil + } +} + +func (ec *EppoClient) logBanditAction(ctx context.Context, event BanditEvent) { + // need to catch panics from Logger and continue + defer func() { + r := recover() + if r != nil { + fmt.Println("panic occurred:", r) + } + }() + + switch { + case ec.loggerContext != nil: + if l, ok := ec.loggerContext.(interface { + LogBanditAction(context.Context, BanditEvent) + }); ok { + l.LogBanditAction(ctx, event) + } + case ec.logger != nil: + if l, ok := ec.logger.(BanditActionLogger); ok { + l.LogBanditAction(event) + } + } } diff --git a/eppoclient/client_test.go b/eppoclient/client_test.go index 2391147..053f254 100644 --- a/eppoclient/client_test.go +++ b/eppoclient/client_test.go @@ -1,6 +1,7 @@ package eppoclient import ( + "context" "testing" "time" @@ -16,7 +17,7 @@ var ( func Test_AssignBlankExperiment(t *testing.T) { var mockLogger = new(mockLogger) - client := newEppoClient(newConfigurationStore(), nil, nil, mockLogger, applicationLogger) + client := newEppoClient(newConfigurationStore(), nil, nil, mockLogger, nil, applicationLogger) _, err := client.GetStringAssignment("", "subject-1", Attributes{}, "") assert.Error(t, err) @@ -24,7 +25,7 @@ func Test_AssignBlankExperiment(t *testing.T) { func Test_AssignBlankSubject(t *testing.T) { var mockLogger = new(mockLogger) - client := newEppoClient(newConfigurationStore(), nil, nil, mockLogger, applicationLogger) + client := newEppoClient(newConfigurationStore(), nil, nil, mockLogger, nil, applicationLogger) _, err := client.GetStringAssignment("experiment-1", "", Attributes{}, "") assert.Error(t, err) @@ -97,7 +98,7 @@ func Test_LogAssignment(t *testing.T) { }, } - client := newEppoClient(newConfigurationStoreWithConfig(configuration{flags: config}), nil, nil, mockLogger, applicationLogger) + client := newEppoClient(newConfigurationStoreWithConfig(configuration{flags: config}), nil, nil, mockLogger, nil, applicationLogger) assignment, err := client.GetStringAssignment("experiment-key-1", "user-1", Attributes{}, "") expected := "control" @@ -109,6 +110,98 @@ func Test_LogAssignment(t *testing.T) { } } +func Test_LogAssignmentContext(t *testing.T) { + tests := []struct { + name string + doLog *bool + expectedCalls int + }{ + { + name: "DoLog key is absent", + doLog: nil, + expectedCalls: 1, + }, + { + name: "DoLog key is present but false", + doLog: &[]bool{false}[0], + expectedCalls: 0, + }, + { + name: "DoLog key is present and true", + doLog: &[]bool{true}[0], + expectedCalls: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + mockLoggerContext = new(mockLoggerContext) + mockLogger = new(mockLogger) + ) + mockLoggerContext.Mock. + On("LogAssignment", mock.Anything, mock.Anything). + Return() + + config := configResponse{ + Flags: map[string]*flagConfiguration{ + "experiment-key-1": &flagConfiguration{ + Key: "experiment-key-1", + Enabled: true, + TotalShards: 10000, + VariationType: stringVariation, + Variations: map[string]variation{ + "control": variation{ + Key: "control", + Value: []byte("\"control\""), + }, + }, + Allocations: []allocation{ + { + Key: "allocation-key", + DoLog: tt.doLog, + Splits: []split{ + { + VariationKey: "control", + Shards: []shard{ + { + Salt: "", + Ranges: []shardRange{ + { + Start: 0, + End: 10000, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + client := newEppoClient( + newConfigurationStoreWithConfig(configuration{flags: config}), + nil, + nil, + mockLogger, + mockLoggerContext, + applicationLogger, + ) + + ctx := context.TODO() + assignment, err := client.GetStringAssignmentContext(ctx, "experiment-key-1", "user-1", Attributes{}, "") + expected := "control" + + assert.Nil(t, err) + assert.Equal(t, expected, assignment) + mockLoggerContext.AssertNumberOfCalls(t, "LogAssignment", tt.expectedCalls) + }) + } +} + func Test_client_loggerIsCalledWithProperBanditEvent(t *testing.T) { var logger = new(mockLogger) logger.Mock.On("LogAssignment", mock.Anything).Return() @@ -142,12 +235,14 @@ func Test_client_loggerIsCalledWithProperBanditEvent(t *testing.T) { }, } - client := newEppoClient(newConfigurationStoreWithConfig(configuration{flags: flags, bandits: bandits}), nil, nil, logger, applicationLogger) + client := newEppoClient(newConfigurationStoreWithConfig(configuration{flags: flags, bandits: bandits}), nil, nil, logger, nil, applicationLogger) actions := map[string]ContextAttributes{ "action1": {}, } client.GetBanditAction("testFlag", "subject", ContextAttributes{}, actions, "bandit") + t.Log(len(logger.Calls)) + event := logger.Calls[0].Arguments[0].(BanditEvent) assert.Equal(t, "testFlag", event.FlagKey) assert.Equal(t, "bandit", event.BanditKey) @@ -155,6 +250,62 @@ func Test_client_loggerIsCalledWithProperBanditEvent(t *testing.T) { assert.Equal(t, "action1", event.Action) } +func Test_client_loggerContextIsCalledWithProperBanditEvent(t *testing.T) { + var ( + logger = new(mockLogger) + loggerContext = new(mockLoggerContext) + ) + loggerContext.Mock.On("LogAssignment", mock.Anything, mock.Anything).Return() + loggerContext.Mock.On("LogBanditAction", mock.Anything, mock.Anything).Return() + + flags := configResponse{ + Bandits: map[string][]banditVariation{ + "bandit": []banditVariation{ + banditVariation{ + Key: "bandit", + FlagKey: "testFlag", + VariationKey: "bandit", + VariationValue: "bandit", + }, + }, + }, + } + bandits := banditResponse{ + Bandits: map[string]banditConfiguration{ + "bandit": { + BanditKey: "bandit", + ModelName: "falcon", + ModelVersion: "v123", + ModelData: banditModelData{ + Gamma: 0, + DefaultActionScore: 0, + ActionProbabilityFloor: 0, + Coefficients: map[string]banditCoefficients{}, + }, + }, + }, + } + + client := newEppoClient( + newConfigurationStoreWithConfig(configuration{flags: flags, bandits: bandits}), + nil, + nil, + logger, + loggerContext, + applicationLogger, + ) + actions := map[string]ContextAttributes{ + "action1": {}, + } + client.GetBanditAction("testFlag", "subject", ContextAttributes{}, actions, "bandit") + + event := loggerContext.Calls[0].Arguments[1].(BanditEvent) + assert.Equal(t, "testFlag", event.FlagKey) + assert.Equal(t, "bandit", event.BanditKey) + assert.Equal(t, "subject", event.Subject) + assert.Equal(t, "action1", event.Action) +} + func Test_GetStringAssignmentHandlesLoggingPanic(t *testing.T) { var mockLogger = new(mockLogger) mockLogger.Mock.On("LogAssignment", mock.Anything).Panic("logging panic") @@ -195,7 +346,7 @@ func Test_GetStringAssignmentHandlesLoggingPanic(t *testing.T) { }, }} - client := newEppoClient(newConfigurationStoreWithConfig(configuration{flags: config}), nil, nil, mockLogger, applicationLogger) + client := newEppoClient(newConfigurationStoreWithConfig(configuration{flags: config}), nil, nil, mockLogger, nil, applicationLogger) assignment, err := client.GetStringAssignment("experiment-key-1", "user-1", Attributes{}, "") expected := "control" @@ -237,7 +388,7 @@ func Test_client_handlesBanditLoggerPanic(t *testing.T) { }, } - client := newEppoClient(newConfigurationStoreWithConfig(configuration{flags: flags, bandits: bandits}), nil, nil, logger, applicationLogger) + client := newEppoClient(newConfigurationStoreWithConfig(configuration{flags: flags, bandits: bandits}), nil, nil, logger, nil, applicationLogger) actions := map[string]ContextAttributes{ "action1": {}, } @@ -279,7 +430,7 @@ func Test_client_correctActionIsReturnedIfBanditLoggerPanics(t *testing.T) { }, } - client := newEppoClient(newConfigurationStoreWithConfig(configuration{flags: flags, bandits: bandits}), nil, nil, logger, applicationLogger) + client := newEppoClient(newConfigurationStoreWithConfig(configuration{flags: flags, bandits: bandits}), nil, nil, logger, nil, applicationLogger) actions := map[string]ContextAttributes{ "action1": {}, } @@ -294,7 +445,7 @@ func Test_client_correctActionIsReturnedIfBanditLoggerPanics(t *testing.T) { func Test_Initialized_timeout(t *testing.T) { var mockLogger = new(mockLogger) - client := newEppoClient(newConfigurationStore(), nil, nil, mockLogger, applicationLogger) + client := newEppoClient(newConfigurationStore(), nil, nil, mockLogger, nil, applicationLogger) timedOut := false select { @@ -310,7 +461,7 @@ func Test_Initialized_timeout(t *testing.T) { func Test_Initialized_success(t *testing.T) { var mockLogger = new(mockLogger) configurationStore := newConfigurationStore() - client := newEppoClient(configurationStore, nil, nil, mockLogger, applicationLogger) + client := newEppoClient(configurationStore, nil, nil, mockLogger, nil, applicationLogger) go func() { <-time.After(1 * time.Microsecond) diff --git a/eppoclient/config.go b/eppoclient/config.go index 617f855..b53d6b7 100644 --- a/eppoclient/config.go +++ b/eppoclient/config.go @@ -12,12 +12,13 @@ const defaultBaseUrl = "https://fscdn.eppo.cloud/api" const defaultPollerInterval = 10 * time.Second type Config struct { - BaseUrl string - SdkKey string - AssignmentLogger IAssignmentLogger - PollerInterval time.Duration - ApplicationLogger ApplicationLogger - HttpClient *http.Client + BaseUrl string + SdkKey string + AssignmentLogger IAssignmentLogger + AssignmentLoggerContext IAssignmentLoggerContext + PollerInterval time.Duration + ApplicationLogger ApplicationLogger + HttpClient *http.Client } func (cfg *Config) validate() error { diff --git a/eppoclient/initclient.go b/eppoclient/initclient.go index 53b3810..59cded0 100644 --- a/eppoclient/initclient.go +++ b/eppoclient/initclient.go @@ -24,10 +24,15 @@ func InitClient(config Config) (*EppoClient, error) { configStore := newConfigurationStore() requestor := newConfigurationRequestor(*httpClient, configStore, applicationLogger) - assignmentLogger := config.AssignmentLogger - poller := newPoller(config.PollerInterval, requestor.FetchAndStoreConfigurations, applicationLogger) - client := newEppoClient(configStore, requestor, poller, assignmentLogger, applicationLogger) + client := newEppoClient( + configStore, + requestor, + poller, + config.AssignmentLogger, + config.AssignmentLoggerContext, + applicationLogger, + ) client.poller.Start() diff --git a/eppoclient/mocks_test.go b/eppoclient/mocks_test.go index c017732..b7b8d67 100644 --- a/eppoclient/mocks_test.go +++ b/eppoclient/mocks_test.go @@ -1,6 +1,8 @@ package eppoclient import ( + "context" + "github.com/stretchr/testify/mock" ) @@ -16,6 +18,18 @@ func (ml *mockLogger) LogBanditAction(event BanditEvent) { ml.MethodCalled("LogBanditAction", event) } +type mockLoggerContext struct { + mock.Mock +} + +func (ml *mockLoggerContext) LogAssignment(ctx context.Context, event AssignmentEvent) { + ml.MethodCalled("LogAssignment", ctx, event) +} + +func (ml *mockLoggerContext) LogBanditAction(ctx context.Context, event BanditEvent) { + ml.MethodCalled("LogBanditAction", ctx, event) +} + // `mockNonBanditLogger` is missing `LogBanditAction` and therefore // does not implement `BanditActionLogger`. type mockNonBanditLogger struct {