Skip to content
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

[] Pulak/test fsc #395

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
136 changes: 112 additions & 24 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,18 @@ const (

// OptimizelyClient is the entry point to the Optimizely SDK
type OptimizelyClient struct {
ctx context.Context
ConfigManager config.ProjectConfigManager
DecisionService decision.Service
EventProcessor event.Processor
OdpManager odp.Manager
notificationCenter notification.Center
execGroup *utils.ExecGroup
logger logging.OptimizelyLogProducer
defaultDecideOptions *decide.Options
tracer tracing.Tracer
ctx context.Context
ConfigManager config.ProjectConfigManager
DecisionService decision.Service
DecisionServiceWithoutUPS decision.Service
UserProfileService decision.UserProfileService
EventProcessor event.Processor
OdpManager odp.Manager
notificationCenter notification.Center
execGroup *utils.ExecGroup
logger logging.OptimizelyLogProducer
defaultDecideOptions *decide.Options
tracer tracing.Tracer
}

// CreateUserContext creates a context of the user for which decision APIs will be called.
Expand All @@ -130,7 +132,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, userProfile *decision.UserProfile, key string, options *decide.Options) OptimizelyDecision {
var err error
defer func() {
if r := recover(); r != nil {
Expand Down Expand Up @@ -179,26 +181,21 @@ func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string,
var featureDecision decision.FeatureDecision
var reasons decide.DecisionReasons

// To avoid cyclo-complexity warning
findRegularDecision := func() {
// regular decision
featureDecision, reasons, err = o.DecisionService.GetFeatureDecision(decisionContext, usrContext, &allOptions)
decisionReasons.Append(reasons)
}

// check forced-decisions first
// Passing empty rule-key because checking mapping with flagKey only
if userContext.forcedDecisionService != nil {
var variation *entities.Variation
variation, reasons, err = userContext.forcedDecisionService.FindValidatedForcedDecision(projectConfig, decision.OptimizelyDecisionContext{FlagKey: key, RuleKey: ""}, &allOptions)
decisionReasons.Append(reasons)
if err != nil {
findRegularDecision()
featureDecision, reasons, err = o.findRegularDecision(decisionContext, usrContext, userProfile, &allOptions)
decisionReasons.Append(reasons)
} else {
featureDecision = decision.FeatureDecision{Decision: decision.Decision{Reason: pkgReasons.ForcedDecisionFound}, Variation: variation, Source: decision.FeatureTest}
}
} else {
findRegularDecision()
featureDecision, reasons, err = o.findRegularDecision(decisionContext, usrContext, userProfile, &allOptions)
decisionReasons.Append(reasons)
}

if err != nil {
Expand Down Expand Up @@ -238,6 +235,77 @@ func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string,
return NewOptimizelyDecision(variationKey, ruleKey, key, flagEnabled, optimizelyJSON, userContext, reasonsToReport)
}

func (o *OptimizelyClient) findRegularDecision(decisionContext decision.FeatureDecisionContext, userContext entities.UserContext, userProfile *decision.UserProfile, options *decide.Options) (decision.FeatureDecision, decide.DecisionReasons, error) {
if o.UserProfileService == nil || options.IgnoreUserProfileService {
return o.DecisionService.GetFeatureDecision(decisionContext, userContext, options)
}

featureDecision := decision.FeatureDecision{}
if decisionContext.Feature != nil {
for _, featureExperiment := range decisionContext.Feature.FeatureExperiments {
decisionKey := decision.NewUserDecisionKey(featureExperiment.ID)

fx := featureExperiment
experimentDecisionContext := decision.ExperimentDecisionContext{
ProjectConfig: decisionContext.ProjectConfig,
Experiment: &fx,
}

expDecision, reasons, err := decision.NewExperimentWhitelistService().GetDecision(experimentDecisionContext, userContext, options)
if err == nil && expDecision.Variation != nil {
featureDecision.Variation = expDecision.Variation
featureDecision.Experiment = featureExperiment
featureDecision.Source = decision.FeatureTest
return featureDecision, reasons, nil
}

if decisionContext.ForcedDecisionService != nil {
forcedDecision, _reasons, err := decisionContext.ForcedDecisionService.FindValidatedForcedDecision(decisionContext.ProjectConfig, decision.OptimizelyDecisionContext{FlagKey: decisionContext.Feature.Key, RuleKey: featureExperiment.Key}, options)
reasons.Append(_reasons)
if err == nil {
return decision.FeatureDecision{
Experiment: featureExperiment,
Variation: forcedDecision,
Source: decision.FeatureTest,
}, reasons, nil
}
}

if savedVariationID, ok := userProfile.ExperimentBucketMap[decisionKey]; ok {
if variation, ok := featureExperiment.Variations[savedVariationID]; ok {
featureDecision.Variation = &variation
infoMessage := reasons.AddInfo(`User "%s" was previously bucketed into variation "%s" of experiment "%s".`, userContext.ID, variation.Key, featureExperiment.Key)
o.logger.Debug(infoMessage)
} else {
warningMessage := reasons.AddInfo(`User "%s" was previously bucketed into variation with ID "%s" for experiment "%s", but no matching variation was found.`, userContext.ID, savedVariationID, featureExperiment.Key)
o.logger.Warning(warningMessage)
}
}

if featureDecision.Variation != nil {
featureDecision.Experiment = featureExperiment
featureDecision.Source = decision.FeatureTest
return featureDecision, reasons, nil
}

}
}

// if no saved decision found, bucket the user
featureDecision, reason, err := o.DecisionServiceWithoutUPS.GetFeatureDecision(decisionContext, userContext, options)
if err != nil {
return featureDecision, reason, err
}
if featureDecision.Variation != nil {
decisionKey := decision.NewUserDecisionKey(featureDecision.Experiment.ID)
if userProfile.ExperimentBucketMap == nil {
userProfile.ExperimentBucketMap = make(map[decision.UserDecisionKey]string)
}
userProfile.ExperimentBucketMap[decisionKey] = featureDecision.Variation.ID
}
return featureDecision, reason, nil
}

func (o *OptimizelyClient) decideForKeys(userContext OptimizelyUserContext, keys []string, options *decide.Options) map[string]OptimizelyDecision {
var err error
defer func() {
Expand Down Expand Up @@ -269,11 +337,31 @@ func (o *OptimizelyClient) decideForKeys(userContext OptimizelyUserContext, keys
return decisionMap
}

enabledFlagsOnly := o.getAllOptions(options).EnabledFlagsOnly
allOptions := o.getAllOptions(options)

var userProfile *decision.UserProfile
userProfileBucketLen := 0
ignoreUserProfileSvc := o.UserProfileService == nil || allOptions.IgnoreUserProfileService
if !ignoreUserProfileSvc {
up := o.UserProfileService.Lookup(userContext.GetUserID())
if up.ID == "" {
up = decision.UserProfile{
ID: userContext.GetUserID(),
ExperimentBucketMap: make(map[decision.UserDecisionKey]string),
}
}
userProfile = &up
userProfileBucketLen = len(userProfile.ExperimentBucketMap)
}

for _, key := range keys {
optimizelyDecision := o.decide(userContext, key, options)
if !enabledFlagsOnly || optimizelyDecision.Enabled {
decisionMap[key] = optimizelyDecision
optimizelyDecision := o.decide(userContext, userProfile, key, options)
decisionMap[key] = optimizelyDecision
}

if !ignoreUserProfileSvc {
if userProfile != nil && len(userProfile.ExperimentBucketMap) > userProfileBucketLen {
o.UserProfileService.Save(*userProfile)
}
}

Expand Down
11 changes: 11 additions & 0 deletions pkg/client/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,17 @@ func (f *OptimizelyFactory) Client(clientOptions ...OptionFunc) (*OptimizelyClie
appClient.EventProcessor = event.NewBatchEventProcessor(eventProcessorOptions...)
}

if f.userProfileService != nil {
appClient.UserProfileService = f.userProfileService
var experimentServiceOptions []decision.CESOptionFunc
if f.overrideStore != nil {
experimentServiceOptions = append(experimentServiceOptions, decision.WithOverrideStore(f.overrideStore))
}
compositeExperimentServiceWithoutUPS := decision.NewCompositeExperimentService(f.SDKKey, experimentServiceOptions...)
compositeServiceWithoutUPS := decision.NewCompositeService(f.SDKKey, decision.WithCompositeExperimentService(compositeExperimentServiceWithoutUPS))
appClient.DecisionServiceWithoutUPS = compositeServiceWithoutUPS
}

if f.decisionService != nil {
appClient.DecisionService = f.decisionService
} else {
Expand Down
26 changes: 23 additions & 3 deletions pkg/client/optimizely_user_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,21 +130,31 @@ 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.
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)

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())
return o.optimizely.decideForKeys(userContextCopy, keys, convertDecideOptions(options))
decideOptions := convertDecideOptions(options)
decisionMap := o.optimizely.decideForKeys(userContextCopy, keys, decideOptions)

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
Expand Down Expand Up @@ -208,3 +218,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
}
Loading