From cfed748b754cd5ab91f5d78a414c77b8cf616182 Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Tue, 22 Oct 2024 20:29:54 +0600 Subject: [PATCH 01/19] add support for batch UPS for decideAll & decideForKeys --- pkg/client/client.go | 22 ++++++++++++++++++- pkg/client/factory.go | 4 ++++ pkg/client/optimizely_user_context.go | 7 +++++- pkg/decision/entities.go | 2 ++ pkg/decision/feature_experiment_service.go | 1 + pkg/decision/persisting_experiment_service.go | 20 +++++++++++------ 6 files changed, 47 insertions(+), 9 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index 61fc8174..a78e3926 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -104,6 +104,7 @@ type OptimizelyClient struct { ctx context.Context ConfigManager config.ProjectConfigManager DecisionService decision.Service + UserProfileService decision.UserProfileService EventProcessor event.Processor OdpManager odp.Manager notificationCenter notification.Center @@ -153,6 +154,7 @@ func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string, decisionContext := decision.FeatureDecisionContext{ ForcedDecisionService: userContext.forcedDecisionService, + UserProfile: userContext.userProfile, } projectConfig, err := o.getProjectConfig() if err != nil { @@ -268,8 +270,19 @@ func (o *OptimizelyClient) decideForKeys(userContext OptimizelyUserContext, keys if len(keys) == 0 { return decisionMap } + allOptions := o.getAllOptions(options) + + var userProfile *decision.UserProfile + userProfileLen := 0 + ignoreUserProfileSvc := o.UserProfileService == nil || allOptions.IgnoreUserProfileService + if !ignoreUserProfileSvc { + up := o.UserProfileService.Lookup(userContext.GetUserID()) + userProfile = &up + userContext.userProfile = userProfile + userProfileLen = len(userProfile.ExperimentBucketMap) + } - enabledFlagsOnly := o.getAllOptions(options).EnabledFlagsOnly + enabledFlagsOnly := allOptions.EnabledFlagsOnly for _, key := range keys { optimizelyDecision := o.decide(userContext, key, options) if !enabledFlagsOnly || optimizelyDecision.Enabled { @@ -277,6 +290,13 @@ func (o *OptimizelyClient) decideForKeys(userContext OptimizelyUserContext, keys } } + if !ignoreUserProfileSvc { + isUserProfileUpdated := userProfile != nil && len(userProfile.ExperimentBucketMap) != userProfileLen + if isUserProfileUpdated { + o.UserProfileService.Save(*userProfile) + } + } + return decisionMap } diff --git a/pkg/client/factory.go b/pkg/client/factory.go index fd9d62ed..e7e8dd1d 100644 --- a/pkg/client/factory.go +++ b/pkg/client/factory.go @@ -145,6 +145,10 @@ func (f *OptimizelyFactory) Client(clientOptions ...OptionFunc) (*OptimizelyClie appClient.EventProcessor = event.NewBatchEventProcessor(eventProcessorOptions...) } + if f.userProfileService != nil { + appClient.UserProfileService = f.userProfileService + } + if f.decisionService != nil { appClient.DecisionService = f.decisionService } else { diff --git a/pkg/client/optimizely_user_context.go b/pkg/client/optimizely_user_context.go index 1b592247..cd2244e6 100644 --- a/pkg/client/optimizely_user_context.go +++ b/pkg/client/optimizely_user_context.go @@ -35,6 +35,7 @@ type OptimizelyUserContext struct { qualifiedSegments []string optimizely *OptimizelyClient forcedDecisionService *pkgDecision.ForcedDecisionService + userProfile *pkgDecision.UserProfile mutex *sync.RWMutex } @@ -130,7 +131,11 @@ func (o *OptimizelyUserContext) IsQualifiedFor(segment string) bool { func (o *OptimizelyUserContext) Decide(key string, options []decide.OptimizelyDecideOptions) OptimizelyDecision { // use a copy of the user context so that any changes to the original context are not reflected inside the decision userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService(), o.GetQualifiedSegments()) - return o.optimizely.decide(userContextCopy, key, convertDecideOptions(options)) + decision, found := o.optimizely.decideForKeys(userContextCopy, []string{key}, convertDecideOptions(options))[key] + if !found { + return NewErrorDecision(key, *o, decide.GetDecideError(decide.SDKNotReady)) + } + return decision } // DecideAll returns a key-map of decision results for all active flag keys with options. diff --git a/pkg/decision/entities.go b/pkg/decision/entities.go index 63139329..1dcd7915 100644 --- a/pkg/decision/entities.go +++ b/pkg/decision/entities.go @@ -27,6 +27,7 @@ import ( type ExperimentDecisionContext struct { Experiment *entities.Experiment ProjectConfig config.ProjectConfig + UserProfile *UserProfile } // FeatureDecisionContext contains the information needed to be able to make a decision for a given feature @@ -35,6 +36,7 @@ type FeatureDecisionContext struct { ProjectConfig config.ProjectConfig Variable entities.Variable ForcedDecisionService *ForcedDecisionService + UserProfile *UserProfile } // UnsafeFeatureDecisionInfo represents response for GetDetailedFeatureDecisionUnsafe api diff --git a/pkg/decision/feature_experiment_service.go b/pkg/decision/feature_experiment_service.go index e9f07ec6..f8c7132b 100644 --- a/pkg/decision/feature_experiment_service.go +++ b/pkg/decision/feature_experiment_service.go @@ -64,6 +64,7 @@ func (f FeatureExperimentService) GetDecision(decisionContext FeatureDecisionCon experimentDecisionContext := ExperimentDecisionContext{ Experiment: &experiment, ProjectConfig: decisionContext.ProjectConfig, + UserProfile: decisionContext.UserProfile, } experimentDecision, decisionReasons, err := f.compositeExperimentService.GetDecision(experimentDecisionContext, userContext, options) diff --git a/pkg/decision/persisting_experiment_service.go b/pkg/decision/persisting_experiment_service.go index b27f8414..e4e3996f 100644 --- a/pkg/decision/persisting_experiment_service.go +++ b/pkg/decision/persisting_experiment_service.go @@ -52,7 +52,7 @@ func (p PersistingExperimentService) GetDecision(decisionContext ExperimentDecis return p.experimentBucketedService.GetDecision(decisionContext, userContext, options) } - var userProfile UserProfile + var userProfile *UserProfile var decisionReasons decide.DecisionReasons // check to see if there is a saved decision for the user experimentDecision, userProfile, decisionReasons = p.getSavedDecision(decisionContext, userContext, options) @@ -66,16 +66,20 @@ func (p PersistingExperimentService) GetDecision(decisionContext ExperimentDecis if experimentDecision.Variation != nil { // save decision if a user profile service is provided userProfile.ID = userContext.ID - p.saveDecision(userProfile, decisionContext.Experiment, experimentDecision) + p.saveDecision(userProfile, decisionContext, experimentDecision) } return experimentDecision, reasons, err } -func (p PersistingExperimentService) getSavedDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext, options *decide.Options) (ExperimentDecision, UserProfile, decide.DecisionReasons) { +func (p PersistingExperimentService) getSavedDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext, options *decide.Options) (ExperimentDecision, *UserProfile, decide.DecisionReasons) { reasons := decide.NewDecisionReasons(options) experimentDecision := ExperimentDecision{} - userProfile := p.userProfileService.Lookup(userContext.ID) + userProfile := decisionContext.UserProfile + if userProfile == nil { + up := p.userProfileService.Lookup(userContext.ID) + userProfile = &up + } // look up experiment decision from user profile decisionKey := NewUserDecisionKey(decisionContext.Experiment.ID) @@ -97,14 +101,16 @@ func (p PersistingExperimentService) getSavedDecision(decisionContext Experiment return experimentDecision, userProfile, reasons } -func (p PersistingExperimentService) saveDecision(userProfile UserProfile, experiment *entities.Experiment, decision ExperimentDecision) { +func (p PersistingExperimentService) saveDecision(userProfile *UserProfile, decisionContext ExperimentDecisionContext, decision ExperimentDecision) { if p.userProfileService != nil { - decisionKey := NewUserDecisionKey(experiment.ID) + decisionKey := NewUserDecisionKey(decisionContext.Experiment.ID) if userProfile.ExperimentBucketMap == nil { userProfile.ExperimentBucketMap = map[UserDecisionKey]string{} } userProfile.ExperimentBucketMap[decisionKey] = decision.Variation.ID - p.userProfileService.Save(userProfile) + if decisionContext.UserProfile == nil { + p.userProfileService.Save(*userProfile) + } p.logger.Debug(fmt.Sprintf(`Decision saved for user %q.`, userProfile.ID)) } } From 85f2395b00d82724c30c79b0aa2e00003e6f25a4 Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Tue, 22 Oct 2024 21:05:32 +0600 Subject: [PATCH 02/19] update decideForKeys logic Signed-off-by: pulak-opti --- pkg/client/client.go | 5 +---- pkg/client/optimizely_user_context.go | 27 +++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index a78e3926..0acbdd9d 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -282,12 +282,9 @@ func (o *OptimizelyClient) decideForKeys(userContext OptimizelyUserContext, keys userProfileLen = len(userProfile.ExperimentBucketMap) } - enabledFlagsOnly := allOptions.EnabledFlagsOnly for _, key := range keys { optimizelyDecision := o.decide(userContext, key, options) - if !enabledFlagsOnly || optimizelyDecision.Enabled { - decisionMap[key] = optimizelyDecision - } + decisionMap[key] = optimizelyDecision } if !ignoreUserProfileSvc { diff --git a/pkg/client/optimizely_user_context.go b/pkg/client/optimizely_user_context.go index cd2244e6..b356aa22 100644 --- a/pkg/client/optimizely_user_context.go +++ b/pkg/client/optimizely_user_context.go @@ -142,14 +142,37 @@ func (o *OptimizelyUserContext) Decide(key string, options []decide.OptimizelyDe func (o *OptimizelyUserContext) DecideAll(options []decide.OptimizelyDecideOptions) map[string]OptimizelyDecision { // use a copy of the user context so that any changes to the original context are not reflected inside the decision userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService(), o.GetQualifiedSegments()) - return o.optimizely.decideAll(userContextCopy, convertDecideOptions(options)) + decideOptions := convertDecideOptions(options) + decisionMap := o.optimizely.decideAll(userContextCopy, decideOptions) + allOptions := o.optimizely.getAllOptions(decideOptions) + enabledFlagsOnly := allOptions.EnabledFlagsOnly + + filteredDecision := make(map[string]OptimizelyDecision) + for key, decision := range decisionMap { + if !enabledFlagsOnly || decision.Enabled { + filteredDecision[key] = decision + } + } + return filteredDecision } // DecideForKeys returns a key-map of decision results for multiple flag keys and options. func (o *OptimizelyUserContext) DecideForKeys(keys []string, options []decide.OptimizelyDecideOptions) map[string]OptimizelyDecision { // use a copy of the user context so that any changes to the original context are not reflected inside the decision userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService(), o.GetQualifiedSegments()) - return o.optimizely.decideForKeys(userContextCopy, keys, convertDecideOptions(options)) + decisionMap := o.optimizely.decideForKeys(userContextCopy, keys, convertDecideOptions(options)) + + decideOptions := convertDecideOptions(options) + allOptions := o.optimizely.getAllOptions(decideOptions) + enabledFlagsOnly := allOptions.EnabledFlagsOnly + + filteredDecision := make(map[string]OptimizelyDecision) + for key, decision := range decisionMap { + if !enabledFlagsOnly || decision.Enabled { + filteredDecision[key] = decision + } + } + return filteredDecision } // TrackEvent generates a conversion event with the given event key if it exists and queues it up to be sent to the Optimizely From 0b21b609750727201dd99cd1f9f405917a794a0c Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Tue, 22 Oct 2024 21:29:42 +0600 Subject: [PATCH 03/19] refactor code Signed-off-by: pulak-opti --- pkg/client/optimizely_user_context.go | 33 ++++++++----------- pkg/decision/persisting_experiment_service.go | 21 +++++++----- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/pkg/client/optimizely_user_context.go b/pkg/client/optimizely_user_context.go index b356aa22..e6254346 100644 --- a/pkg/client/optimizely_user_context.go +++ b/pkg/client/optimizely_user_context.go @@ -144,35 +144,18 @@ func (o *OptimizelyUserContext) DecideAll(options []decide.OptimizelyDecideOptio userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService(), o.GetQualifiedSegments()) decideOptions := convertDecideOptions(options) decisionMap := o.optimizely.decideAll(userContextCopy, decideOptions) - allOptions := o.optimizely.getAllOptions(decideOptions) - enabledFlagsOnly := allOptions.EnabledFlagsOnly - filteredDecision := make(map[string]OptimizelyDecision) - for key, decision := range decisionMap { - if !enabledFlagsOnly || decision.Enabled { - filteredDecision[key] = decision - } - } - return filteredDecision + return filteredDecision(decisionMap, o.optimizely.getAllOptions(decideOptions).EnabledFlagsOnly) } // DecideForKeys returns a key-map of decision results for multiple flag keys and options. func (o *OptimizelyUserContext) DecideForKeys(keys []string, options []decide.OptimizelyDecideOptions) map[string]OptimizelyDecision { // use a copy of the user context so that any changes to the original context are not reflected inside the decision userContextCopy := newOptimizelyUserContext(o.GetOptimizely(), o.GetUserID(), o.GetUserAttributes(), o.getForcedDecisionService(), o.GetQualifiedSegments()) - decisionMap := o.optimizely.decideForKeys(userContextCopy, keys, convertDecideOptions(options)) - decideOptions := convertDecideOptions(options) - allOptions := o.optimizely.getAllOptions(decideOptions) - enabledFlagsOnly := allOptions.EnabledFlagsOnly + decisionMap := o.optimizely.decideForKeys(userContextCopy, keys, decideOptions) - filteredDecision := make(map[string]OptimizelyDecision) - for key, decision := range decisionMap { - if !enabledFlagsOnly || decision.Enabled { - filteredDecision[key] = decision - } - } - return filteredDecision + return filteredDecision(decisionMap, o.optimizely.getAllOptions(decideOptions).EnabledFlagsOnly) } // TrackEvent generates a conversion event with the given event key if it exists and queues it up to be sent to the Optimizely @@ -236,3 +219,13 @@ func copyQualifiedSegments(qualifiedSegments []string) (qualifiedSegmentsCopy [] copy(qualifiedSegmentsCopy, qualifiedSegments) return } + +func filteredDecision(decisionMap map[string]OptimizelyDecision, enabledFlagsOnly bool) map[string]OptimizelyDecision { + filteredDecision := make(map[string]OptimizelyDecision) + for key, decision := range decisionMap { + if !enabledFlagsOnly || decision.Enabled { + filteredDecision[key] = decision + } + } + return filteredDecision +} diff --git a/pkg/decision/persisting_experiment_service.go b/pkg/decision/persisting_experiment_service.go index e4e3996f..c8632ed4 100644 --- a/pkg/decision/persisting_experiment_service.go +++ b/pkg/decision/persisting_experiment_service.go @@ -52,7 +52,7 @@ func (p PersistingExperimentService) GetDecision(decisionContext ExperimentDecis return p.experimentBucketedService.GetDecision(decisionContext, userContext, options) } - var userProfile *UserProfile + var userProfile UserProfile var decisionReasons decide.DecisionReasons // check to see if there is a saved decision for the user experimentDecision, userProfile, decisionReasons = p.getSavedDecision(decisionContext, userContext, options) @@ -72,13 +72,14 @@ func (p PersistingExperimentService) GetDecision(decisionContext ExperimentDecis return experimentDecision, reasons, err } -func (p PersistingExperimentService) getSavedDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext, options *decide.Options) (ExperimentDecision, *UserProfile, decide.DecisionReasons) { +func (p PersistingExperimentService) getSavedDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext, options *decide.Options) (ExperimentDecision, UserProfile, decide.DecisionReasons) { reasons := decide.NewDecisionReasons(options) experimentDecision := ExperimentDecision{} - userProfile := decisionContext.UserProfile - if userProfile == nil { - up := p.userProfileService.Lookup(userContext.ID) - userProfile = &up + var userProfile UserProfile + if decisionContext.UserProfile == nil { + userProfile = p.userProfileService.Lookup(userContext.ID) + } else { + userProfile = *decisionContext.UserProfile } // look up experiment decision from user profile @@ -101,15 +102,17 @@ func (p PersistingExperimentService) getSavedDecision(decisionContext Experiment return experimentDecision, userProfile, reasons } -func (p PersistingExperimentService) saveDecision(userProfile *UserProfile, decisionContext ExperimentDecisionContext, decision ExperimentDecision) { +func (p PersistingExperimentService) saveDecision(userProfile UserProfile, decisionContext ExperimentDecisionContext, decision ExperimentDecision) { if p.userProfileService != nil { decisionKey := NewUserDecisionKey(decisionContext.Experiment.ID) if userProfile.ExperimentBucketMap == nil { userProfile.ExperimentBucketMap = map[UserDecisionKey]string{} } - userProfile.ExperimentBucketMap[decisionKey] = decision.Variation.ID if decisionContext.UserProfile == nil { - p.userProfileService.Save(*userProfile) + userProfile.ExperimentBucketMap[decisionKey] = decision.Variation.ID + p.userProfileService.Save(userProfile) + } else { + decisionContext.UserProfile.ExperimentBucketMap[decisionKey] = decision.Variation.ID } p.logger.Debug(fmt.Sprintf(`Decision saved for user %q.`, userProfile.ID)) } From 11c0ebe3073161a3afa7d44d085f5cb8c668cb6d Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Tue, 22 Oct 2024 21:42:41 +0600 Subject: [PATCH 04/19] fix bug Signed-off-by: pulak-opti --- pkg/decision/persisting_experiment_service.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/decision/persisting_experiment_service.go b/pkg/decision/persisting_experiment_service.go index c8632ed4..83276725 100644 --- a/pkg/decision/persisting_experiment_service.go +++ b/pkg/decision/persisting_experiment_service.go @@ -112,6 +112,9 @@ func (p PersistingExperimentService) saveDecision(userProfile UserProfile, decis userProfile.ExperimentBucketMap[decisionKey] = decision.Variation.ID p.userProfileService.Save(userProfile) } else { + if decisionContext.UserProfile.ExperimentBucketMap == nil { + decisionContext.UserProfile.ExperimentBucketMap = make(map[UserDecisionKey]string) + } decisionContext.UserProfile.ExperimentBucketMap[decisionKey] = decision.Variation.ID } p.logger.Debug(fmt.Sprintf(`Decision saved for user %q.`, userProfile.ID)) From d6982f4ac01f054bc306853880992f3d11f2ab1f Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Tue, 22 Oct 2024 22:00:57 +0600 Subject: [PATCH 05/19] use deepcopy for userprofile Signed-off-by: pulak-opti --- pkg/decision/entities.go | 19 +++++++++++++++++++ pkg/decision/persisting_experiment_service.go | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/pkg/decision/entities.go b/pkg/decision/entities.go index 1dcd7915..34396312 100644 --- a/pkg/decision/entities.go +++ b/pkg/decision/entities.go @@ -95,3 +95,22 @@ type UserProfile struct { ID string ExperimentBucketMap map[UserDecisionKey]string } + +// DeepCopy creates a deep copy of the UserProfile. +func (up *UserProfile) DeepCopy() *UserProfile { + if up == nil { + return nil + } + + copy := *up + + // Deep copy the ExperimentBucketMap + if up.ExperimentBucketMap != nil { + copy.ExperimentBucketMap = make(map[UserDecisionKey]string) + for k, v := range up.ExperimentBucketMap { + copy.ExperimentBucketMap[k] = v + } + } + + return © +} diff --git a/pkg/decision/persisting_experiment_service.go b/pkg/decision/persisting_experiment_service.go index 83276725..12ac50b0 100644 --- a/pkg/decision/persisting_experiment_service.go +++ b/pkg/decision/persisting_experiment_service.go @@ -79,7 +79,7 @@ func (p PersistingExperimentService) getSavedDecision(decisionContext Experiment if decisionContext.UserProfile == nil { userProfile = p.userProfileService.Lookup(userContext.ID) } else { - userProfile = *decisionContext.UserProfile + userProfile = *decisionContext.UserProfile.DeepCopy() } // look up experiment decision from user profile From 27caf566d5ea05962d0c6df8439ec863bfeecf9e Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Tue, 22 Oct 2024 22:04:46 +0600 Subject: [PATCH 06/19] fix lint error Signed-off-by: pulak-opti --- pkg/decision/entities.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/decision/entities.go b/pkg/decision/entities.go index 34396312..a80a68b3 100644 --- a/pkg/decision/entities.go +++ b/pkg/decision/entities.go @@ -102,15 +102,15 @@ func (up *UserProfile) DeepCopy() *UserProfile { return nil } - copy := *up + userProfileCopy := *up // Deep copy the ExperimentBucketMap if up.ExperimentBucketMap != nil { - copy.ExperimentBucketMap = make(map[UserDecisionKey]string) + userProfileCopy.ExperimentBucketMap = make(map[UserDecisionKey]string) for k, v := range up.ExperimentBucketMap { - copy.ExperimentBucketMap[k] = v + userProfileCopy.ExperimentBucketMap[k] = v } } - return © + return &userProfileCopy } From f2af569660cd089a3d7f0436dfec463dbd402bad Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Tue, 22 Oct 2024 23:13:13 +0600 Subject: [PATCH 07/19] fix bug Signed-off-by: pulak-opti --- pkg/client/client.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index 0acbdd9d..95c4ea5d 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -131,7 +131,7 @@ func (o *OptimizelyClient) WithTraceContext(ctx context.Context) *OptimizelyClie return o } -func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string, options *decide.Options) OptimizelyDecision { +func (o *OptimizelyClient) decide(userContext *OptimizelyUserContext, key string, options *decide.Options) OptimizelyDecision { var err error defer func() { if r := recover(); r != nil { @@ -158,13 +158,13 @@ func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string, } projectConfig, err := o.getProjectConfig() if err != nil { - return NewErrorDecision(key, userContext, decide.GetDecideError(decide.SDKNotReady)) + return NewErrorDecision(key, *userContext, decide.GetDecideError(decide.SDKNotReady)) } decisionContext.ProjectConfig = projectConfig feature, err := projectConfig.GetFeatureByKey(key) if err != nil { - return NewErrorDecision(key, userContext, decide.GetDecideError(decide.FlagKeyInvalid, key)) + return NewErrorDecision(key, *userContext, decide.GetDecideError(decide.FlagKeyInvalid, key)) } decisionContext.Feature = &feature @@ -237,7 +237,7 @@ func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string, } } - return NewOptimizelyDecision(variationKey, ruleKey, key, flagEnabled, optimizelyJSON, userContext, reasonsToReport) + return NewOptimizelyDecision(variationKey, ruleKey, key, flagEnabled, optimizelyJSON, *userContext, reasonsToReport) } func (o *OptimizelyClient) decideForKeys(userContext OptimizelyUserContext, keys []string, options *decide.Options) map[string]OptimizelyDecision { @@ -277,13 +277,19 @@ func (o *OptimizelyClient) decideForKeys(userContext OptimizelyUserContext, keys ignoreUserProfileSvc := o.UserProfileService == nil || allOptions.IgnoreUserProfileService if !ignoreUserProfileSvc { up := o.UserProfileService.Lookup(userContext.GetUserID()) + if up.ID == "" { + up = decision.UserProfile{ + ID: userContext.GetUserID(), + ExperimentBucketMap: map[decision.UserDecisionKey]string{}, + } + } userProfile = &up userContext.userProfile = userProfile userProfileLen = len(userProfile.ExperimentBucketMap) } for _, key := range keys { - optimizelyDecision := o.decide(userContext, key, options) + optimizelyDecision := o.decide(&userContext, key, options) decisionMap[key] = optimizelyDecision } From ee1a45705b900bd3160233a4a7efcf67182b57b9 Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Wed, 23 Oct 2024 00:03:27 +0600 Subject: [PATCH 08/19] fix Signed-off-by: pulak-opti --- pkg/decision/rollout_service.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/decision/rollout_service.go b/pkg/decision/rollout_service.go index 13c1402c..e06d9ded 100644 --- a/pkg/decision/rollout_service.go +++ b/pkg/decision/rollout_service.go @@ -69,6 +69,7 @@ func (r RolloutService) GetDecision(decisionContext FeatureDecisionContext, user return ExperimentDecisionContext{ Experiment: experiment, ProjectConfig: decisionContext.ProjectConfig, + UserProfile: decisionContext.UserProfile, } } From 71ef7193adc4a91c07c9ff5b2eb2e97309865769 Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Wed, 23 Oct 2024 11:40:17 +0600 Subject: [PATCH 09/19] fix Signed-off-by: pulak-opti --- pkg/decision/rollout_service.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/decision/rollout_service.go b/pkg/decision/rollout_service.go index e06d9ded..13c1402c 100644 --- a/pkg/decision/rollout_service.go +++ b/pkg/decision/rollout_service.go @@ -69,7 +69,6 @@ func (r RolloutService) GetDecision(decisionContext FeatureDecisionContext, user return ExperimentDecisionContext{ Experiment: experiment, ProjectConfig: decisionContext.ProjectConfig, - UserProfile: decisionContext.UserProfile, } } From 48bd159973c7ffed013183e94e22a511b3849fa2 Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Wed, 23 Oct 2024 12:07:49 +0600 Subject: [PATCH 10/19] fix --- pkg/decision/entities.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/decision/entities.go b/pkg/decision/entities.go index a80a68b3..c1c5afd9 100644 --- a/pkg/decision/entities.go +++ b/pkg/decision/entities.go @@ -102,7 +102,9 @@ func (up *UserProfile) DeepCopy() *UserProfile { return nil } - userProfileCopy := *up + userProfileCopy := UserProfile{ + ID: up.ID, + } // Deep copy the ExperimentBucketMap if up.ExperimentBucketMap != nil { From 62eb35ef8cf8305ee2e594af88c65afe37cf538c Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Wed, 23 Oct 2024 12:47:55 +0600 Subject: [PATCH 11/19] test Signed-off-by: pulak-opti --- pkg/decision/persisting_experiment_service.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/decision/persisting_experiment_service.go b/pkg/decision/persisting_experiment_service.go index 12ac50b0..a3a0c346 100644 --- a/pkg/decision/persisting_experiment_service.go +++ b/pkg/decision/persisting_experiment_service.go @@ -116,6 +116,7 @@ func (p PersistingExperimentService) saveDecision(userProfile UserProfile, decis decisionContext.UserProfile.ExperimentBucketMap = make(map[UserDecisionKey]string) } decisionContext.UserProfile.ExperimentBucketMap[decisionKey] = decision.Variation.ID + p.userProfileService.Save(*decisionContext.UserProfile) } p.logger.Debug(fmt.Sprintf(`Decision saved for user %q.`, userProfile.ID)) } From 36a9604cc9ebcc403c93e938c401a33b429e765c Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Wed, 23 Oct 2024 13:44:05 +0600 Subject: [PATCH 12/19] refactor code Signed-off-by: pulak-opti --- pkg/decision/persisting_experiment_service.go | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/pkg/decision/persisting_experiment_service.go b/pkg/decision/persisting_experiment_service.go index a3a0c346..6d8a4427 100644 --- a/pkg/decision/persisting_experiment_service.go +++ b/pkg/decision/persisting_experiment_service.go @@ -52,6 +52,8 @@ func (p PersistingExperimentService) GetDecision(decisionContext ExperimentDecis return p.experimentBucketedService.GetDecision(decisionContext, userContext, options) } + isUserProfileNil := decisionContext.UserProfile == nil + var userProfile UserProfile var decisionReasons decide.DecisionReasons // check to see if there is a saved decision for the user @@ -66,7 +68,15 @@ func (p PersistingExperimentService) GetDecision(decisionContext ExperimentDecis if experimentDecision.Variation != nil { // save decision if a user profile service is provided userProfile.ID = userContext.ID - p.saveDecision(userProfile, decisionContext, experimentDecision) + decisionKey := NewUserDecisionKey(decisionContext.Experiment.ID) + if isUserProfileNil { + p.saveDecision(userProfile, decisionKey, experimentDecision) + } else { + if decisionContext.UserProfile.ExperimentBucketMap == nil { + decisionContext.UserProfile.ExperimentBucketMap = make(map[UserDecisionKey]string) + } + decisionContext.UserProfile.ExperimentBucketMap[decisionKey] = experimentDecision.Variation.ID + } } return experimentDecision, reasons, err @@ -102,22 +112,13 @@ func (p PersistingExperimentService) getSavedDecision(decisionContext Experiment return experimentDecision, userProfile, reasons } -func (p PersistingExperimentService) saveDecision(userProfile UserProfile, decisionContext ExperimentDecisionContext, decision ExperimentDecision) { +func (p PersistingExperimentService) saveDecision(userProfile UserProfile, decisionKey UserDecisionKey, decision ExperimentDecision) { if p.userProfileService != nil { - decisionKey := NewUserDecisionKey(decisionContext.Experiment.ID) if userProfile.ExperimentBucketMap == nil { userProfile.ExperimentBucketMap = map[UserDecisionKey]string{} } - if decisionContext.UserProfile == nil { - userProfile.ExperimentBucketMap[decisionKey] = decision.Variation.ID - p.userProfileService.Save(userProfile) - } else { - if decisionContext.UserProfile.ExperimentBucketMap == nil { - decisionContext.UserProfile.ExperimentBucketMap = make(map[UserDecisionKey]string) - } - decisionContext.UserProfile.ExperimentBucketMap[decisionKey] = decision.Variation.ID - p.userProfileService.Save(*decisionContext.UserProfile) - } + userProfile.ExperimentBucketMap[decisionKey] = decision.Variation.ID + p.userProfileService.Save(userProfile) p.logger.Debug(fmt.Sprintf(`Decision saved for user %q.`, userProfile.ID)) } } From 443fc54f7bd4a23d760ab3b06b325d9903a3e070 Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Wed, 23 Oct 2024 17:07:27 +0600 Subject: [PATCH 13/19] fix Signed-off-by: pulak-opti --- pkg/client/client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/client/client.go b/pkg/client/client.go index 95c4ea5d..cd7d0587 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -1099,6 +1099,7 @@ func (o *OptimizelyClient) getExperimentDecision(experimentKey string, userConte decisionContext = decision.ExperimentDecisionContext{ Experiment: &experiment, ProjectConfig: projectConfig, + UserProfile: nil, } options := &decide.Options{} From b99cca76509c94e20518546233f1891da802f5db Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Tue, 29 Oct 2024 09:32:51 +0600 Subject: [PATCH 14/19] save profile everytime Signed-off-by: pulak-opti --- pkg/client/client.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index cd7d0587..08a3b117 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -273,7 +273,6 @@ func (o *OptimizelyClient) decideForKeys(userContext OptimizelyUserContext, keys allOptions := o.getAllOptions(options) var userProfile *decision.UserProfile - userProfileLen := 0 ignoreUserProfileSvc := o.UserProfileService == nil || allOptions.IgnoreUserProfileService if !ignoreUserProfileSvc { up := o.UserProfileService.Lookup(userContext.GetUserID()) @@ -285,7 +284,6 @@ func (o *OptimizelyClient) decideForKeys(userContext OptimizelyUserContext, keys } userProfile = &up userContext.userProfile = userProfile - userProfileLen = len(userProfile.ExperimentBucketMap) } for _, key := range keys { @@ -293,11 +291,8 @@ func (o *OptimizelyClient) decideForKeys(userContext OptimizelyUserContext, keys decisionMap[key] = optimizelyDecision } - if !ignoreUserProfileSvc { - isUserProfileUpdated := userProfile != nil && len(userProfile.ExperimentBucketMap) != userProfileLen - if isUserProfileUpdated { - o.UserProfileService.Save(*userProfile) - } + if !ignoreUserProfileSvc && userProfile != nil { + o.UserProfileService.Save(*userProfile) } return decisionMap From 5ea39ee08b8c87bb11ad214179bac379dad07533 Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Tue, 29 Oct 2024 09:49:52 +0600 Subject: [PATCH 15/19] update tests Signed-off-by: pulak-opti --- pkg/client/optimizely_user_context_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/client/optimizely_user_context_test.go b/pkg/client/optimizely_user_context_test.go index e85fe676..ec29bda2 100644 --- a/pkg/client/optimizely_user_context_test.go +++ b/pkg/client/optimizely_user_context_test.go @@ -884,7 +884,6 @@ func (s *OptimizelyUserContextTestSuite) TestDecideOptionsBypassUps() { // should return variationId2 set by UPS s.Equal(variationKey2, decision.VariationKey) userProfileService.AssertCalled(s.T(), "Lookup", s.userID) - userProfileService.AssertNotCalled(s.T(), "Save", mock.Anything) options = append(options, decide.IgnoreUserProfileService) decision = userContext.Decide(flagKey, options) @@ -894,8 +893,6 @@ func (s *OptimizelyUserContextTestSuite) TestDecideOptionsBypassUps() { // should not lookup, ignore variationId2 set by UPS and return variationId1 s.Equal(variationKey1, decision.VariationKey) userProfileService.AssertNumberOfCalls(s.T(), "Lookup", 1) - // also should not save either - userProfileService.AssertNotCalled(s.T(), "Save", mock.Anything) } func (s *OptimizelyUserContextTestSuite) TestDecideOptionsExcludeVariables() { From 353830d037554c1336f368e90565702931b5b210 Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Tue, 29 Oct 2024 10:19:21 +0600 Subject: [PATCH 16/19] remove deepcopy method Signed-off-by: pulak-opti --- pkg/decision/entities.go | 21 ------------------- pkg/decision/persisting_experiment_service.go | 2 +- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/pkg/decision/entities.go b/pkg/decision/entities.go index c1c5afd9..1dcd7915 100644 --- a/pkg/decision/entities.go +++ b/pkg/decision/entities.go @@ -95,24 +95,3 @@ type UserProfile struct { ID string ExperimentBucketMap map[UserDecisionKey]string } - -// DeepCopy creates a deep copy of the UserProfile. -func (up *UserProfile) DeepCopy() *UserProfile { - if up == nil { - return nil - } - - userProfileCopy := UserProfile{ - ID: up.ID, - } - - // Deep copy the ExperimentBucketMap - if up.ExperimentBucketMap != nil { - userProfileCopy.ExperimentBucketMap = make(map[UserDecisionKey]string) - for k, v := range up.ExperimentBucketMap { - userProfileCopy.ExperimentBucketMap[k] = v - } - } - - return &userProfileCopy -} diff --git a/pkg/decision/persisting_experiment_service.go b/pkg/decision/persisting_experiment_service.go index 6d8a4427..5e52f229 100644 --- a/pkg/decision/persisting_experiment_service.go +++ b/pkg/decision/persisting_experiment_service.go @@ -89,7 +89,7 @@ func (p PersistingExperimentService) getSavedDecision(decisionContext Experiment if decisionContext.UserProfile == nil { userProfile = p.userProfileService.Lookup(userContext.ID) } else { - userProfile = *decisionContext.UserProfile.DeepCopy() + userProfile = *decisionContext.UserProfile } // look up experiment decision from user profile From 13968cf259f92a3dc17e2d81d903442b4195c45c Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Tue, 29 Oct 2024 11:57:19 +0600 Subject: [PATCH 17/19] update unit tests Signed-off-by: pulak-opti --- pkg/client/optimizely_user_context_test.go | 72 ++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/pkg/client/optimizely_user_context_test.go b/pkg/client/optimizely_user_context_test.go index ec29bda2..0962a715 100644 --- a/pkg/client/optimizely_user_context_test.go +++ b/pkg/client/optimizely_user_context_test.go @@ -1218,6 +1218,78 @@ func (s *OptimizelyUserContextTestSuite) TestForcedDecision() { s.Error(err) } +func (s *OptimizelyUserContextTestSuite) TestDecideAllFlagsWithBatchUPS() { + experimentID1 := "10420810910" + experimentID2 := "10420810911" + experimentID3 := "10420810912" + variationID1 := "10418510623" + variationID2 := "10418510624" + variationID3 := "10418510625" + + userProfileService := new(MockUserProfileService) + s.OptimizelyClient, _ = s.factory.Client( + WithEventProcessor(s.eventProcessor), + WithUserProfileService(userProfileService), + ) + + decisionKey1 := decision.NewUserDecisionKey(experimentID1) + decisionKey2 := decision.NewUserDecisionKey(experimentID2) + decisionKey3 := decision.NewUserDecisionKey(experimentID3) + savedUserProfile := decision.UserProfile{ + ID: s.userID, + ExperimentBucketMap: map[decision.UserDecisionKey]string{ + decisionKey1: variationID1, + decisionKey2: variationID2, + decisionKey3: variationID3, + }, + } + userProfileService.On("Lookup", s.userID).Return(savedUserProfile) + userProfileService.On("Save", mock.Anything) + + user := s.OptimizelyClient.CreateUserContext(s.userID, map[string]interface{}{"gender": "f"}) + decisions := user.DecideAll(nil) + s.Len(decisions, 3) + + userProfileService.AssertNumberOfCalls(s.T(), "Lookup", 1) + userProfileService.AssertNumberOfCalls(s.T(), "Save", 1) +} + +func (s *OptimizelyUserContextTestSuite) TestDecideForKeysFlagsWithBatchUPS() { + experimentID1 := "10420810910" + experimentID2 := "10420810911" + experimentID3 := "10420810912" + variationID1 := "10418510623" + variationID2 := "10418510624" + variationID3 := "10418510625" + + userProfileService := new(MockUserProfileService) + s.OptimizelyClient, _ = s.factory.Client( + WithEventProcessor(s.eventProcessor), + WithUserProfileService(userProfileService), + ) + + decisionKey1 := decision.NewUserDecisionKey(experimentID1) + decisionKey2 := decision.NewUserDecisionKey(experimentID2) + decisionKey3 := decision.NewUserDecisionKey(experimentID3) + savedUserProfile := decision.UserProfile{ + ID: s.userID, + ExperimentBucketMap: map[decision.UserDecisionKey]string{ + decisionKey1: variationID1, + decisionKey2: variationID2, + decisionKey3: variationID3, + }, + } + userProfileService.On("Lookup", s.userID).Return(savedUserProfile) + userProfileService.On("Save", mock.Anything) + + user := s.OptimizelyClient.CreateUserContext(s.userID, map[string]interface{}{"gender": "f"}) + decisions := user.DecideForKeys([]string{"feature-1", "feature-2", "feature-3"}, nil) + s.Len(decisions, 3) + + userProfileService.AssertNumberOfCalls(s.T(), "Lookup", 1) + userProfileService.AssertNumberOfCalls(s.T(), "Save", 1) +} + func TestOptimizelyUserContextTestSuite(t *testing.T) { suite.Run(t, new(OptimizelyUserContextTestSuite)) } From 43748f64d3e4331b9327779dbd655e94b4c00931 Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Tue, 29 Oct 2024 12:31:51 +0600 Subject: [PATCH 18/19] update unit tests Signed-off-by: pulak-opti --- pkg/client/client.go | 3 +- pkg/client/optimizely_user_context_test.go | 57 ++----------------- pkg/decision/entities.go | 1 + pkg/decision/persisting_experiment_service.go | 1 + 4 files changed, 8 insertions(+), 54 deletions(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index 08a3b117..9010ad44 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -291,8 +291,9 @@ func (o *OptimizelyClient) decideForKeys(userContext OptimizelyUserContext, keys decisionMap[key] = optimizelyDecision } - if !ignoreUserProfileSvc && userProfile != nil { + if !ignoreUserProfileSvc && userProfile != nil && userProfile.HasUnsavedChange { o.UserProfileService.Save(*userProfile) + userProfile.HasUnsavedChange = false } return decisionMap diff --git a/pkg/client/optimizely_user_context_test.go b/pkg/client/optimizely_user_context_test.go index 0962a715..add091b5 100644 --- a/pkg/client/optimizely_user_context_test.go +++ b/pkg/client/optimizely_user_context_test.go @@ -884,6 +884,7 @@ func (s *OptimizelyUserContextTestSuite) TestDecideOptionsBypassUps() { // should return variationId2 set by UPS s.Equal(variationKey2, decision.VariationKey) userProfileService.AssertCalled(s.T(), "Lookup", s.userID) + userProfileService.AssertNotCalled(s.T(), "Save", mock.Anything) options = append(options, decide.IgnoreUserProfileService) decision = userContext.Decide(flagKey, options) @@ -893,6 +894,8 @@ func (s *OptimizelyUserContextTestSuite) TestDecideOptionsBypassUps() { // should not lookup, ignore variationId2 set by UPS and return variationId1 s.Equal(variationKey1, decision.VariationKey) userProfileService.AssertNumberOfCalls(s.T(), "Lookup", 1) + // also should not save either + userProfileService.AssertNotCalled(s.T(), "Save", mock.Anything) } func (s *OptimizelyUserContextTestSuite) TestDecideOptionsExcludeVariables() { @@ -1219,34 +1222,18 @@ func (s *OptimizelyUserContextTestSuite) TestForcedDecision() { } func (s *OptimizelyUserContextTestSuite) TestDecideAllFlagsWithBatchUPS() { - experimentID1 := "10420810910" - experimentID2 := "10420810911" - experimentID3 := "10420810912" - variationID1 := "10418510623" - variationID2 := "10418510624" - variationID3 := "10418510625" - userProfileService := new(MockUserProfileService) s.OptimizelyClient, _ = s.factory.Client( WithEventProcessor(s.eventProcessor), WithUserProfileService(userProfileService), ) - - decisionKey1 := decision.NewUserDecisionKey(experimentID1) - decisionKey2 := decision.NewUserDecisionKey(experimentID2) - decisionKey3 := decision.NewUserDecisionKey(experimentID3) savedUserProfile := decision.UserProfile{ ID: s.userID, - ExperimentBucketMap: map[decision.UserDecisionKey]string{ - decisionKey1: variationID1, - decisionKey2: variationID2, - decisionKey3: variationID3, - }, } userProfileService.On("Lookup", s.userID).Return(savedUserProfile) userProfileService.On("Save", mock.Anything) - user := s.OptimizelyClient.CreateUserContext(s.userID, map[string]interface{}{"gender": "f"}) + user := s.OptimizelyClient.CreateUserContext(s.userID, nil) decisions := user.DecideAll(nil) s.Len(decisions, 3) @@ -1254,42 +1241,6 @@ func (s *OptimizelyUserContextTestSuite) TestDecideAllFlagsWithBatchUPS() { userProfileService.AssertNumberOfCalls(s.T(), "Save", 1) } -func (s *OptimizelyUserContextTestSuite) TestDecideForKeysFlagsWithBatchUPS() { - experimentID1 := "10420810910" - experimentID2 := "10420810911" - experimentID3 := "10420810912" - variationID1 := "10418510623" - variationID2 := "10418510624" - variationID3 := "10418510625" - - userProfileService := new(MockUserProfileService) - s.OptimizelyClient, _ = s.factory.Client( - WithEventProcessor(s.eventProcessor), - WithUserProfileService(userProfileService), - ) - - decisionKey1 := decision.NewUserDecisionKey(experimentID1) - decisionKey2 := decision.NewUserDecisionKey(experimentID2) - decisionKey3 := decision.NewUserDecisionKey(experimentID3) - savedUserProfile := decision.UserProfile{ - ID: s.userID, - ExperimentBucketMap: map[decision.UserDecisionKey]string{ - decisionKey1: variationID1, - decisionKey2: variationID2, - decisionKey3: variationID3, - }, - } - userProfileService.On("Lookup", s.userID).Return(savedUserProfile) - userProfileService.On("Save", mock.Anything) - - user := s.OptimizelyClient.CreateUserContext(s.userID, map[string]interface{}{"gender": "f"}) - decisions := user.DecideForKeys([]string{"feature-1", "feature-2", "feature-3"}, nil) - s.Len(decisions, 3) - - userProfileService.AssertNumberOfCalls(s.T(), "Lookup", 1) - userProfileService.AssertNumberOfCalls(s.T(), "Save", 1) -} - func TestOptimizelyUserContextTestSuite(t *testing.T) { suite.Run(t, new(OptimizelyUserContextTestSuite)) } diff --git a/pkg/decision/entities.go b/pkg/decision/entities.go index 1dcd7915..bebfe5f4 100644 --- a/pkg/decision/entities.go +++ b/pkg/decision/entities.go @@ -94,4 +94,5 @@ func NewUserDecisionKey(experimentID string) UserDecisionKey { type UserProfile struct { ID string ExperimentBucketMap map[UserDecisionKey]string + HasUnsavedChange bool } diff --git a/pkg/decision/persisting_experiment_service.go b/pkg/decision/persisting_experiment_service.go index 5e52f229..72f7d589 100644 --- a/pkg/decision/persisting_experiment_service.go +++ b/pkg/decision/persisting_experiment_service.go @@ -76,6 +76,7 @@ func (p PersistingExperimentService) GetDecision(decisionContext ExperimentDecis decisionContext.UserProfile.ExperimentBucketMap = make(map[UserDecisionKey]string) } decisionContext.UserProfile.ExperimentBucketMap[decisionKey] = experimentDecision.Variation.ID + decisionContext.UserProfile.HasUnsavedChange = true } } From 06238e99daab8faabf925fa8ed5a26108d4a8831 Mon Sep 17 00:00:00 2001 From: pulak-opti Date: Wed, 30 Oct 2024 00:01:31 +0600 Subject: [PATCH 19/19] add unit tests --- pkg/client/optimizely_user_context_test.go | 73 +++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/pkg/client/optimizely_user_context_test.go b/pkg/client/optimizely_user_context_test.go index add091b5..f74b83cc 100644 --- a/pkg/client/optimizely_user_context_test.go +++ b/pkg/client/optimizely_user_context_test.go @@ -1223,10 +1223,13 @@ func (s *OptimizelyUserContextTestSuite) TestForcedDecision() { func (s *OptimizelyUserContextTestSuite) TestDecideAllFlagsWithBatchUPS() { userProfileService := new(MockUserProfileService) - s.OptimizelyClient, _ = s.factory.Client( + var err error + s.OptimizelyClient, err = s.factory.Client( WithEventProcessor(s.eventProcessor), WithUserProfileService(userProfileService), ) + s.Nil(err) + savedUserProfile := decision.UserProfile{ ID: s.userID, } @@ -1241,6 +1244,74 @@ func (s *OptimizelyUserContextTestSuite) TestDecideAllFlagsWithBatchUPS() { userProfileService.AssertNumberOfCalls(s.T(), "Save", 1) } +func (s *OptimizelyUserContextTestSuite) TestDecideForKeysWithBatchUPS() { + flagKey1 := "feature_1" + experimentID1 := "10390977673" + variationKey1 := "18257766532" + variationID1 := "variation_with_traffic" + flagKey2 := "feature_2" // embedding experiment: "exp_no_audience" + experimentID2 := "10420810910" + variationID2 := "10418510624" + variationKey2 := "variation_no_traffic" + userProfileService := new(MockUserProfileService) + var err error + s.OptimizelyClient, err = s.factory.Client( + WithEventProcessor(s.eventProcessor), + WithUserProfileService(userProfileService), + ) + s.Nil(err) + + savedUserProfile := decision.UserProfile{ + ID: s.userID, + ExperimentBucketMap: map[decision.UserDecisionKey]string{ + decision.NewUserDecisionKey(experimentID1): variationID1, + decision.NewUserDecisionKey(experimentID2): variationID2, + }, + } + userProfileService.On("Lookup", s.userID).Return(savedUserProfile) + userProfileService.On("Save", mock.Anything) + + user := s.OptimizelyClient.CreateUserContext(s.userID, nil) + decisions := user.DecideForKeys([]string{flagKey1, flagKey2}, nil) + s.Len(decisions, 2) + s.Equal(variationKey1, decisions[flagKey1].VariationKey) + s.Equal(variationKey2, decisions[flagKey2].VariationKey) + + userProfileService.AssertNumberOfCalls(s.T(), "Lookup", 1) + userProfileService.AssertNumberOfCalls(s.T(), "Save", 0) +} + +func (s *OptimizelyUserContextTestSuite) TestDecideWithBatchUPS() { + flagKey := "feature_2" // embedding experiment: "exp_no_audience" + experimentID := "10420810910" + variationID2 := "10418510624" + variationKey1 := "variation_no_traffic" + + userProfileService := new(MockUserProfileService) + s.OptimizelyClient, _ = s.factory.Client( + WithEventProcessor(s.eventProcessor), + WithUserProfileService(userProfileService), + ) + + decisionKey := decision.NewUserDecisionKey(experimentID) + savedUserProfile := decision.UserProfile{ + ID: s.userID, + ExperimentBucketMap: map[decision.UserDecisionKey]string{decisionKey: variationID2}, + } + userProfileService.On("Lookup", s.userID).Return(savedUserProfile) + userProfileService.On("Save", mock.Anything) + + client, err := s.factory.Client(WithUserProfileService(userProfileService)) + s.Nil(err) + user := client.CreateUserContext(s.userID, nil) + decision := user.Decide(flagKey, []decide.OptimizelyDecideOptions{decide.IncludeReasons}) + s.Len(decision.Reasons, 1) + + s.Equal(variationKey1, decision.VariationKey) + userProfileService.AssertCalled(s.T(), "Lookup", s.userID) + userProfileService.AssertNotCalled(s.T(), "Save", mock.Anything) +} + func TestOptimizelyUserContextTestSuite(t *testing.T) { suite.Run(t, new(OptimizelyUserContextTestSuite)) }