diff --git a/OptimizelySDK.sln.DotSettings b/OptimizelySDK.sln.DotSettings index 3ccf7ffc..8ee6e5a4 100644 --- a/OptimizelySDK.sln.DotSettings +++ b/OptimizelySDK.sln.DotSettings @@ -43,10 +43,15 @@ <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Enum members"><ElementKinds><Kind Name="ENUM_MEMBER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"><ElementKinds><Kind Name="LOCAL_CONSTANT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Interfaces"><ElementKinds><Kind Name="INTERFACE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /></Policy> True True True True + True True True True diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs index c820fefc..4dba482c 100644 --- a/OptimizelySDK/Bucketing/DecisionService.cs +++ b/OptimizelySDK/Bucketing/DecisionService.cs @@ -43,7 +43,7 @@ public class DecisionService private Bucketer Bucketer; private IErrorHandler ErrorHandler; private UserProfileService UserProfileService; - private ILogger Logger; + private static ILogger Logger; /// /// Associative array of user IDs to an associative array @@ -85,9 +85,9 @@ public DecisionService(Bucketer bucketer, IErrorHandler errorHandler, /// /// Get a Variation of an Experiment for a user to be allocated into. /// - /// The Experiment the user will be bucketed into. - /// Optimizely user context. - /// Project config. + /// The Experiment the user will be bucketed into. + /// Optimizely user context. + /// Project config. /// The Variation the user is allocated into. public virtual Result GetVariation(Experiment experiment, OptimizelyUserContext user, @@ -100,11 +100,11 @@ ProjectConfig config /// /// Get a Variation of an Experiment for a user to be allocated into. /// - /// The Experiment the user will be bucketed into. - /// optimizely user context. - /// Project Config. - /// An array of decision options. - /// The Variation the user is allocated into. + /// The Experiment the user will be bucketed into. + /// Optimizely user context. + /// Project Config. + /// An array of decision options. + /// public virtual Result GetVariation(Experiment experiment, OptimizelyUserContext user, ProjectConfig config, @@ -112,12 +112,60 @@ OptimizelyDecideOption[] options ) { var reasons = new DecisionReasons(); - var userId = user.GetUserId(); + + var ignoreUps = options.Contains(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE); + UserProfileTracker userProfileTracker = null; + + if (UserProfileService != null && !ignoreUps) + { + var userProfile = GetUserProfile(user.GetUserId(), reasons); + userProfileTracker = new UserProfileTracker(userProfile, false); + } + + var response = GetVariation(experiment, user, config, options, userProfileTracker, + reasons); + + if (UserProfileService != null && !ignoreUps && + userProfileTracker?.ProfileUpdated == true) + { + SaveUserProfile(userProfileTracker.UserProfile); + } + + return response; + } + + /// + /// Get a Variation of an Experiment for a user to be allocated into. + /// + /// The Experiment the user will be bucketed into. + /// Optimizely user context. + /// Project Config. + /// An array of decision options. + /// A UserProfileTracker object. + /// Set of reasons for the decision. + /// The Variation the user is allocated into. + public virtual Result GetVariation(Experiment experiment, + OptimizelyUserContext user, + ProjectConfig config, + OptimizelyDecideOption[] options, + UserProfileTracker userProfileTracker, + DecisionReasons reasons = null + ) + { + if (reasons == null) + { + reasons = new DecisionReasons(); + } + if (!ExperimentUtils.IsExperimentActive(experiment, Logger)) { + var message = reasons.AddInfo($"Experiment {experiment.Key} is not running."); + Logger.Log(LogLevel.INFO, message); return Result.NullResult(reasons); } + var userId = user.GetUserId(); + // check if a forced variation is set var decisionVariationResult = GetForcedVariation(experiment.Key, userId, config); reasons += decisionVariationResult.DecisionReasons; @@ -137,76 +185,41 @@ OptimizelyDecideOption[] options return decisionVariationResult; } - // fetch the user profile map from the user profile service - var ignoreUPS = Array.Exists(options, - option => option == OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE); - - UserProfile userProfile = null; - if (!ignoreUPS && UserProfileService != null) + if (userProfileTracker != null) { - try - { - var userProfileMap = UserProfileService.Lookup(user.GetUserId()); - if (userProfileMap != null && - UserProfileUtil.IsValidUserProfileMap(userProfileMap)) - { - userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap); - decisionVariationResult = - GetStoredVariation(experiment, userProfile, config); - reasons += decisionVariationResult.DecisionReasons; - if (decisionVariationResult.ResultObject != null) - { - return decisionVariationResult.SetReasons(reasons); - } - } - else if (userProfileMap == null) - { - Logger.Log(LogLevel.INFO, - reasons.AddInfo( - "We were unable to get a user profile map from the UserProfileService.")); - } - else - { - Logger.Log(LogLevel.ERROR, - reasons.AddInfo("The UserProfileService returned an invalid map.")); - } - } - catch (Exception exception) + decisionVariationResult = + GetStoredVariation(experiment, userProfileTracker.UserProfile, config); + reasons += decisionVariationResult.DecisionReasons; + variation = decisionVariationResult.ResultObject; + if (variation != null) { - Logger.Log(LogLevel.ERROR, reasons.AddInfo(exception.Message)); - ErrorHandler.HandleError( - new Exceptions.OptimizelyRuntimeException(exception.Message)); + return decisionVariationResult; } } - var filteredAttributes = user.GetAttributes(); - var doesUserMeetAudienceConditionsResult = - ExperimentUtils.DoesUserMeetAudienceConditions(config, experiment, user, - LOGGING_KEY_TYPE_EXPERIMENT, experiment.Key, Logger); - reasons += doesUserMeetAudienceConditionsResult.DecisionReasons; - if (doesUserMeetAudienceConditionsResult.ResultObject) + var decisionMeetAudience = ExperimentUtils.DoesUserMeetAudienceConditions(config, + experiment, user, + LOGGING_KEY_TYPE_EXPERIMENT, experiment.Key, Logger); + reasons += decisionMeetAudience.DecisionReasons; + if (decisionMeetAudience.ResultObject) { - // Get Bucketing ID from user attributes. - var bucketingIdResult = GetBucketingId(userId, filteredAttributes); + var bucketingIdResult = GetBucketingId(userId, user.GetAttributes()); reasons += bucketingIdResult.DecisionReasons; decisionVariationResult = Bucketer.Bucket(config, experiment, bucketingIdResult.ResultObject, userId); reasons += decisionVariationResult.DecisionReasons; + variation = decisionVariationResult.ResultObject; - if (decisionVariationResult.ResultObject?.Key != null) + if (variation != null) { - if (UserProfileService != null && !ignoreUPS) + if (userProfileTracker != null) { - var bucketerUserProfile = userProfile ?? - new UserProfile(userId, - new Dictionary()); - SaveVariation(experiment, decisionVariationResult.ResultObject, - bucketerUserProfile); + userProfileTracker.UpdateUserProfile(experiment, variation); } else { - Logger.Log(LogLevel.INFO, + Logger.Log(LogLevel.DEBUG, "This decision will not be saved since the UserProfileService is null."); } } @@ -720,18 +733,6 @@ public virtual Result GetVariationForFeature(FeatureFlag featur new OptimizelyDecideOption[] { }); } - private class UserProfileTracker - { - public UserProfile UserProfile { get; set; } - public bool ProfileUpdated { get; set; } - - public UserProfileTracker(UserProfile userProfile, bool profileUpdated) - { - UserProfile = userProfile; - ProfileUpdated = profileUpdated; - } - } - void SaveUserProfile(UserProfile userProfile) { if (UserProfileService == null) @@ -791,6 +792,40 @@ private UserProfile GetUserProfile(String userId, DecisionReasons reasons) return userProfile; } + public class UserProfileTracker + { + public UserProfile UserProfile { get; set; } + public bool ProfileUpdated { get; set; } + + public UserProfileTracker(UserProfile userProfile, bool profileUpdated) + { + UserProfile = userProfile; + ProfileUpdated = profileUpdated; + } + + public void UpdateUserProfile(Experiment experiment, Variation variation) + { + var experimentId = experiment.Id; + var variationId = variation.Id; + Decision decision; + if (UserProfile.ExperimentBucketMap.ContainsKey(experimentId)) + { + decision = UserProfile.ExperimentBucketMap[experimentId]; + decision.VariationId = variationId; + } + else + { + decision = new Decision(variationId); + } + + UserProfile.ExperimentBucketMap[experimentId] = decision; + ProfileUpdated = true; + + Logger.Log(LogLevel.INFO, + $"Updated variation \"{variationId}\" of experiment \"{experimentId}\" for user \"{UserProfile.UserId}\"."); + } + } + public virtual List> GetVariationsForFeatureList( List featureFlags, OptimizelyUserContext user, @@ -834,22 +869,23 @@ OptimizelyDecideOption[] options decisionResult = GetVariationForFeatureRollout(featureFlag, user, projectConfig); reasons += decisionResult.DecisionReasons; - if (decisionResult.ResultObject != null) + if (decisionResult.ResultObject == null) + { + Logger.Log(LogLevel.INFO, + reasons.AddInfo( + $"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); + decisions.Add(Result.NewResult( + new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT), + reasons)); + } + else { Logger.Log(LogLevel.INFO, reasons.AddInfo( $"The user \"{userId}\" is bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); decisions.Add( Result.NewResult(decisionResult.ResultObject, reasons)); - continue; } - - Logger.Log(LogLevel.INFO, - reasons.AddInfo( - $"The user \"{userId}\" is not bucketed into a rollout for feature flag \"{featureFlag.Key}\".")); - decisions.Add(Result.NewResult( - new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT), - reasons)); } if (UserProfileService != null && !ignoreUPS && userProfileTracker?.ProfileUpdated == true) diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 4bc3b568..a84466e4 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -855,12 +855,30 @@ private OptimizelyUserContext CreateUserContextCopy(string userId, ); } + public FeatureDecision GetForcedDecision(string flagKey, DecisionReasons decisionReasons, + ProjectConfig projectConfig, OptimizelyUserContext user + ) + { + var context = new OptimizelyDecisionContext(flagKey); + var forcedDecisionVariation = + DecisionService.ValidatedForcedDecision(context, projectConfig, user); + decisionReasons += forcedDecisionVariation.DecisionReasons; + if (forcedDecisionVariation.ResultObject != null) + { + return new FeatureDecision(null, forcedDecisionVariation.ResultObject, + FeatureDecision.DECISION_SOURCE_FEATURE_TEST); + } + + return null; + } + /// /// Returns a decision result ({@link OptimizelyDecision}) for a given flag key and a user context, which contains all data required to deliver the flag. ///
    ///
  • If the SDK finds an error, it’ll return a decision with null for variationKey. The decision will include an error message in reasons. ///
///
+ /// User context to be used to make decision. /// A flag key for which a decision will be made. /// A list of options for decision-making. /// A decision result. @@ -869,8 +887,6 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, OptimizelyDecideOption[] options ) { - return DecideForKeys(user, new[] { key }, options)[key]; - var config = ProjectConfigManager?.GetConfig(); if (config == null) @@ -879,141 +895,11 @@ OptimizelyDecideOption[] options ErrorHandler, Logger); } - if (key == null) - { - return OptimizelyDecision.NewErrorDecision(key, - user, - DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, key), - ErrorHandler, Logger); - } - - var flag = config.GetFeatureFlagFromKey(key); - if (flag.Key == null) - { - return OptimizelyDecision.NewErrorDecision(key, - user, - DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, key), - ErrorHandler, Logger); - } - - var userId = user?.GetUserId(); - var userAttributes = user?.GetAttributes(); - var decisionEventDispatched = false; - var allOptions = GetAllOptions(options); - var decisionReasons = new DecisionReasons(); - FeatureDecision decision = null; - - var decisionContext = new OptimizelyDecisionContext(flag.Key); - var forcedDecisionVariation = - DecisionService.ValidatedForcedDecision(decisionContext, config, user); - decisionReasons += forcedDecisionVariation.DecisionReasons; - - if (forcedDecisionVariation.ResultObject != null) - { - decision = new FeatureDecision(null, forcedDecisionVariation.ResultObject, - FeatureDecision.DECISION_SOURCE_FEATURE_TEST); - } - else - { - var flagDecisionResult = DecisionService.GetVariationForFeature( - flag, - user, - config, - userAttributes, - allOptions - ); - decisionReasons += flagDecisionResult.DecisionReasons; - decision = flagDecisionResult.ResultObject; - } - - var featureEnabled = false; - - if (decision?.Variation != null) - { - featureEnabled = decision.Variation.FeatureEnabled.GetValueOrDefault(); - } + var filteredOptions = GetAllOptions(options). + Where(opt => opt != OptimizelyDecideOption.ENABLED_FLAGS_ONLY). + ToArray(); - if (featureEnabled) - { - Logger.Log(LogLevel.INFO, - "Feature \"" + key + "\" is enabled for user \"" + userId + "\""); - } - else - { - Logger.Log(LogLevel.INFO, - "Feature \"" + key + "\" is not enabled for user \"" + userId + "\""); - } - - var variableMap = new Dictionary(); - if (flag?.Variables != null && - !allOptions.Contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) - { - foreach (var featureVariable in flag?.Variables) - { - var variableValue = featureVariable.DefaultValue; - if (featureEnabled) - { - var featureVariableUsageInstance = - decision?.Variation.GetFeatureVariableUsageFromId(featureVariable.Id); - if (featureVariableUsageInstance != null) - { - variableValue = featureVariableUsageInstance.Value; - } - } - - var typeCastedValue = - GetTypeCastedVariableValue(variableValue, featureVariable.Type); - - if (typeCastedValue is OptimizelyJSON) - { - typeCastedValue = ((OptimizelyJSON)typeCastedValue).ToDictionary(); - } - - variableMap.Add(featureVariable.Key, typeCastedValue); - } - } - - var optimizelyJSON = new OptimizelyJSON(variableMap, ErrorHandler, Logger); - - var decisionSource = decision?.Source ?? FeatureDecision.DECISION_SOURCE_ROLLOUT; - if (!allOptions.Contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) - { - decisionEventDispatched = SendImpressionEvent(decision?.Experiment, - decision?.Variation, userId, userAttributes, config, key, decisionSource, - featureEnabled); - } - - var reasonsToReport = decisionReasons - .ToReport(allOptions.Contains(OptimizelyDecideOption.INCLUDE_REASONS)) - .ToArray(); - var variationKey = decision?.Variation?.Key; - - // TODO: add ruleKey values when available later. use a copy of experimentKey until then. - var ruleKey = decision?.Experiment?.Key; - - var decisionInfo = new Dictionary - { - { "flagKey", key }, - { "enabled", featureEnabled }, - { "variables", variableMap }, - { "variationKey", variationKey }, - { "ruleKey", ruleKey }, - { "reasons", reasonsToReport }, - { "decisionEventDispatched", decisionEventDispatched }, - }; - - NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, - DecisionNotificationTypes.FLAG, userId, - userAttributes ?? new UserAttributes(), decisionInfo); - - return new OptimizelyDecision( - variationKey, - featureEnabled, - optimizelyJSON, - ruleKey, - key, - user, - reasonsToReport); + return DecideForKeys(user, new[] { key }, filteredOptions, true)[key]; } internal Dictionary DecideAll(OptimizelyUserContext user, @@ -1038,16 +924,12 @@ OptimizelyDecideOption[] options internal Dictionary DecideForKeys(OptimizelyUserContext user, string[] keys, - OptimizelyDecideOption[] options + OptimizelyDecideOption[] options, + bool ignoreDefaultOptions = false ) { var decisionDictionary = new Dictionary(); - if (keys.Length == 0) - { - return decisionDictionary; - } - var projectConfig = ProjectConfigManager?.GetConfig(); if (projectConfig == null) { @@ -1056,13 +938,20 @@ OptimizelyDecideOption[] options return decisionDictionary; } - var allOptions = GetAllOptions(options); + if (keys.Length == 0) + { + return decisionDictionary; + } + + var allOptions = ignoreDefaultOptions ? options : GetAllOptions(options); var flagDecisions = new Dictionary(); - var decisionReasons = new Dictionary(); + var decisionReasonsMap = new Dictionary(); var flagsWithoutForcedDecisions = new List(); + var validKeys = new List(); + foreach (var key in keys) { var flag = projectConfig.GetFeatureFlagFromKey(key); @@ -1075,40 +964,175 @@ OptimizelyDecideOption[] options continue; } - var decisionContext = new OptimizelyDecisionContext(flag.Key); - var forcedDecisionVariation = - DecisionService.ValidatedForcedDecision(decisionContext, projectConfig, user); - decisionReasons.Add(key, forcedDecisionVariation.DecisionReasons); + validKeys.Add(key); + + var decisionReasons = new DecisionReasons(); + var forcedDecision = GetForcedDecision(key, decisionReasons, projectConfig, user); + decisionReasonsMap.Add(key, decisionReasons); - if (forcedDecisionVariation.ResultObject != null) + if (forcedDecision != null) { - var experiment = projectConfig.GetExperimentFromKey(flag.Key); - var featureDecision = Result.NewResult( - new FeatureDecision(experiment, forcedDecisionVariation.ResultObject, - FeatureDecision.DECISION_SOURCE_FEATURE_TEST), - forcedDecisionVariation.DecisionReasons); - flagDecisions.Add(key, featureDecision.ResultObject); + flagDecisions.Add(key, forcedDecision); } else { flagsWithoutForcedDecisions.Add(flag); } + } - var decisionsList = DecisionService.GetVariationsForFeatureList( - flagsWithoutForcedDecisions, user, projectConfig, user.GetAttributes(), - options); + var decisionsList = DecisionService.GetVariationsForFeatureList( + flagsWithoutForcedDecisions, user, projectConfig, user.GetAttributes(), + allOptions); - var decision = Decide(user, key, options); + for (var i = 0; i < decisionsList.Count; i += 1) + { + var decision = decisionsList[i]; + var flagKey = flagsWithoutForcedDecisions[i].Key; + flagDecisions.Add(flagKey, decision.ResultObject); + decisionReasonsMap[flagKey] += decision.DecisionReasons; + } + + foreach (var key in validKeys) + { + var flagDecision = flagDecisions[key]; + var decisionReasons = decisionReasonsMap[key]; + + var optimizelyDecision = CreateOptimizelyDecision(user, key, flagDecision, + decisionReasons, allOptions.ToList(), projectConfig); if (!allOptions.Contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || - decision.Enabled) + optimizelyDecision.Enabled) { - decisionDictionary.Add(key, decision); + decisionDictionary.Add(key, optimizelyDecision); } } return decisionDictionary; } + private OptimizelyDecision CreateOptimizelyDecision( + OptimizelyUserContext user, + string flagKey, + FeatureDecision flagDecision, + DecisionReasons decisionReasons, + List allOptions, + ProjectConfig projectConfig + ) + { + var userId = user.GetUserId(); + + var flagEnabled = false; + if (flagDecision.Variation != null) + { + if (flagDecision.Variation.IsFeatureEnabled) + { + flagEnabled = true; + } + } + + Logger.Log(LogLevel.INFO, + $"Feature \"{flagKey}\" is enabled for user \"{userId}\"? {flagEnabled}"); + + var variableMap = new Dictionary(); + if (!allOptions.Contains(OptimizelyDecideOption.EXCLUDE_VARIABLES)) + { + var decisionVariables = GetDecisionVariableMap( + projectConfig.GetFeatureFlagFromKey(flagKey), + flagDecision.Variation, + flagEnabled); + variableMap = decisionVariables.ResultObject; + decisionReasons += decisionVariables.DecisionReasons; + } + + var optimizelyJson = new OptimizelyJSON(variableMap, ErrorHandler, Logger); + + var decisionSource = FeatureDecision.DECISION_SOURCE_ROLLOUT; + if (flagDecision.Source != null) + { + decisionSource = flagDecision.Source; + } + + var reasonsToReport = decisionReasons.ToReport().ToArray(); + var variationKey = flagDecision.Variation?.Key; + // TODO: add ruleKey values when available later. use a copy of experimentKey until then. + // add to event metadata as well (currently set to experimentKey) + var ruleKey = flagDecision.Experiment?.Key; + + var decisionEventDispatched = false; + if (!allOptions.Contains(OptimizelyDecideOption.DISABLE_DECISION_EVENT)) + { + decisionEventDispatched = SendImpressionEvent( + flagDecision.Experiment, + flagDecision.Variation, + userId, + user.GetAttributes(), + projectConfig, + flagKey, + decisionSource, + flagEnabled); + } + + var decisionInfo = new Dictionary + { + { "featureKey", flagKey }, + { "featureEnabled", flagEnabled }, + { "variableValues", variableMap }, + { "variationKey", variationKey }, + { "ruleKey", ruleKey }, + { "reasons", reasonsToReport }, + { "decisionEventDispatched", decisionEventDispatched }, + }; + + // var decisionNotificationType = + // projectConfig.IsFeatureExperiment(flagDecision.Experiment?.Id) ? + // DecisionNotificationTypes.FEATURE_TEST : + // DecisionNotificationTypes.AB_TEST; + NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, + userId, + user.GetAttributes(), decisionInfo); + + return new OptimizelyDecision( + variationKey, + flagEnabled, + optimizelyJson, + ruleKey, + flagKey, + user, + reasonsToReport); + } + + private Result> GetDecisionVariableMap(FeatureFlag flag, Variation variation, bool featureEnabled) + { + var reasons = new DecisionReasons(); + var valuesMap = new Dictionary(); + + foreach (var variable in flag.Variables) + { + var value = variable.DefaultValue; + if (featureEnabled) + { + var instance = variation.GetFeatureVariableUsageFromId(variable.Id); + if (instance != null) + { + value = instance.Value; + } + } + + var convertedValue = GetTypeCastedVariableValue(value, variable.Type); + if (convertedValue == null) + { + reasons.AddError(DecisionMessage.Reason(DecisionMessage.VARIABLE_VALUE_INVALID, variable.Key)); + } + else if (convertedValue is OptimizelyJSON optimizelyJson) + { + convertedValue = optimizelyJson.ToDictionary(); + } + + valuesMap[variable.Key] = convertedValue; + } + + return Result>.NewResult(valuesMap, reasons); + } + private OptimizelyDecideOption[] GetAllOptions(OptimizelyDecideOption[] options) { var copiedOptions = DefaultDecideOptions;