/// <summary> /// Get the variation the user is bucketed into for the FeatureFlag /// </summary> /// <param name = "featureFlag" >The feature flag the user wants to access.</param> /// <param name = "userId" >User Identifier</param> /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> /// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is /// successfully bucketed.</returns> public virtual Result <FeatureDecision> GetVariationForFeature(FeatureFlag featureFlag, OptimizelyUserContext user, ProjectConfig config) { return(GetVariationForFeature(featureFlag, user, config, user.GetAttributes(), new OptimizelyDecideOption[] { })); }
/// <summary> /// Get the variation the user is bucketed into for the FeatureFlag /// </summary> /// <param name = "featureFlag" >The feature flag the user wants to access.</param> /// <param name = "userId" >User Identifier</param> /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> /// <returns>null if the user is not bucketed into any variation or the FeatureDecision entity if the user is /// successfully bucketed.</returns> public virtual Result <FeatureDecision> GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes) { return(GetVariationForFeature(featureFlag, userId, config, filteredAttributes, new OptimizelyDecideOption[] { })); }
/// <summary> /// Try to bucket the user into a rollout rule. /// Evaluate the user for rules in priority order by seeing if the user satisfies the audience. /// Fall back onto the everyone else rule if the user is ever excluded from a rule due to traffic allocation. /// </summary> /// <param name = "featureFlag" >The feature flag the user wants to access.</param> /// <param name = "userId" >User Identifier</param> /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> /// <param name = "reasons" >Decision log messages.</param> /// <returns>null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout. /// otherwise the FeatureDecision entity</returns> public virtual Result <FeatureDecision> GetVariationForFeatureRollout(FeatureFlag featureFlag, OptimizelyUserContext user, ProjectConfig config) { var reasons = new DecisionReasons(); if (featureFlag == null) { Logger.Log(LogLevel.ERROR, "Invalid feature flag provided."); return(Result <FeatureDecision> .NullResult(reasons)); } if (string.IsNullOrEmpty(featureFlag.RolloutId)) { Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout.")); return(Result <FeatureDecision> .NullResult(reasons)); } Rollout rollout = config.GetRolloutFromId(featureFlag.RolloutId); if (string.IsNullOrEmpty(rollout.Id)) { Logger.Log(LogLevel.ERROR, reasons.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\"")); return(Result <FeatureDecision> .NullResult(reasons)); } if (rollout.Experiments == null || rollout.Experiments.Count == 0) { return(Result <FeatureDecision> .NullResult(reasons)); } var rolloutRulesLength = rollout.Experiments.Count; var rolloutRules = rollout.Experiments; var userId = user.GetUserId(); var attributes = user.GetAttributes(); var index = 0; while (index < rolloutRulesLength) { // To skip rules var skipToEveryoneElse = false; //Check forced decision first var rule = rolloutRules[index]; var decisionContext = new OptimizelyDecisionContext(featureFlag.Key, rule.Key); var forcedDecisionResponse = user.FindValidatedForcedDecision(decisionContext, config); reasons += forcedDecisionResponse.DecisionReasons; if (forcedDecisionResponse.ResultObject != null) { return(Result <FeatureDecision> .NewResult(new FeatureDecision(rule, forcedDecisionResponse.ResultObject, null), reasons)); } // Regular decision // Get Bucketing ID from user attributes. var bucketingIdResult = GetBucketingId(userId, attributes); reasons += bucketingIdResult.DecisionReasons; var everyoneElse = index == rolloutRulesLength - 1; var loggingKey = everyoneElse ? "Everyone Else" : string.Format("{0}", index + 1); // Evaluate if user meets the audience condition of this rollout rule var doesUserMeetAudienceConditionsResult = ExperimentUtils.DoesUserMeetAudienceConditions(config, rule, attributes, LOGGING_KEY_TYPE_RULE, rule.Key, Logger); reasons += doesUserMeetAudienceConditionsResult.DecisionReasons; if (doesUserMeetAudienceConditionsResult.ResultObject) { Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" meets condition for targeting rule \"{loggingKey}\".")); var bucketedVariation = Bucketer.Bucket(config, rule, bucketingIdResult.ResultObject, userId); reasons += bucketedVariation?.DecisionReasons; if (bucketedVariation?.ResultObject?.Key != null) { Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is in the traffic group of targeting rule \"{loggingKey}\".")); return(Result <FeatureDecision> .NewResult(new FeatureDecision(rule, bucketedVariation.ResultObject, FeatureDecision.DECISION_SOURCE_ROLLOUT), reasons)); } else if (!everyoneElse) { //skip this logging for everyoneElse rule since this has a message not for everyoneElse Logger.Log(LogLevel.INFO, reasons.AddInfo($"User \"{userId}\" is not in the traffic group for targeting rule \"{loggingKey}\". Checking EveryoneElse rule now.")); skipToEveryoneElse = true; } } else { Logger.Log(LogLevel.DEBUG, reasons.AddInfo($"User \"{userId}\" does not meet the conditions for targeting rule \"{loggingKey}\".")); } // the last rule is special for "Everyone Else" index = skipToEveryoneElse ? (rolloutRulesLength - 1) : (index + 1); } return(Result <FeatureDecision> .NullResult(reasons)); }
public static string ToJsonString(this FeatureFlag flag) => DataModelSerialization.SerializeFlag(flag);
/// <summary> /// Try to bucket the user into a rollout rule. /// Evaluate the user for rules in priority order by seeing if the user satisfies the audience. /// Fall back onto the everyone else rule if the user is ever excluded from a rule due to traffic allocation. /// </summary> /// <param name = "featureFlag" >The feature flag the user wants to access.</param> /// <param name = "userId" >User Identifier</param> /// <param name = "filteredAttributes" >The user's attributes. This should be filtered to just attributes in the Datafile.</param> /// <param name = "reasons" >Decision log messages.</param> /// <returns>null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout. /// otherwise the FeatureDecision entity</returns> public virtual Result <FeatureDecision> GetVariationForFeatureRollout(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config) { var reasons = new DecisionReasons(); if (featureFlag == null) { Logger.Log(LogLevel.ERROR, "Invalid feature flag provided."); return(Result <FeatureDecision> .NullResult(reasons)); } if (string.IsNullOrEmpty(featureFlag.RolloutId)) { Logger.Log(LogLevel.INFO, reasons.AddInfo($"The feature flag \"{featureFlag.Key}\" is not used in a rollout.")); return(Result <FeatureDecision> .NullResult(reasons)); } Rollout rollout = config.GetRolloutFromId(featureFlag.RolloutId); if (string.IsNullOrEmpty(rollout.Id)) { Logger.Log(LogLevel.ERROR, reasons.AddInfo($"The rollout with id \"{featureFlag.RolloutId}\" is not found in the datafile for feature flag \"{featureFlag.Key}\"")); return(Result <FeatureDecision> .NullResult(reasons)); } if (rollout.Experiments == null || rollout.Experiments.Count == 0) { return(Result <FeatureDecision> .NullResult(reasons)); } Result <Variation> variationResult = null; var rolloutRulesLength = rollout.Experiments.Count; // Get Bucketing ID from user attributes. var bucketingIdResult = GetBucketingId(userId, filteredAttributes); reasons += bucketingIdResult.DecisionReasons; // For all rules before the everyone else rule for (int i = 0; i < rolloutRulesLength - 1; i++) { string loggingKey = (i + 1).ToString(); var rolloutRule = rollout.Experiments[i]; var userMeetConditionsResult = ExperimentUtils.DoesUserMeetAudienceConditions(config, rolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, loggingKey, Logger); reasons += userMeetConditionsResult.DecisionReasons; if (userMeetConditionsResult.ResultObject) { variationResult = Bucketer.Bucket(config, rolloutRule, bucketingIdResult.ResultObject, userId); reasons += variationResult?.DecisionReasons; if (string.IsNullOrEmpty(variationResult.ResultObject?.Id)) { break; } return(Result <FeatureDecision> .NewResult(new FeatureDecision(rolloutRule, variationResult.ResultObject, FeatureDecision.DECISION_SOURCE_ROLLOUT), reasons)); } else { Logger.Log(LogLevel.DEBUG, $"User \"{userId}\" does not meet the conditions for targeting rule \"{loggingKey}\"."); } } // Get the last rule which is everyone else rule. var everyoneElseRolloutRule = rollout.Experiments[rolloutRulesLength - 1]; var userMeetConditionsResultEveryoneElse = ExperimentUtils.DoesUserMeetAudienceConditions(config, everyoneElseRolloutRule, filteredAttributes, LOGGING_KEY_TYPE_RULE, "Everyone Else", Logger); reasons += userMeetConditionsResultEveryoneElse.DecisionReasons; if (userMeetConditionsResultEveryoneElse.ResultObject) { variationResult = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingIdResult.ResultObject, userId); reasons += variationResult?.DecisionReasons; if (!string.IsNullOrEmpty(variationResult?.ResultObject?.Id)) { Logger.Log(LogLevel.DEBUG, $"User \"{userId}\" meets conditions for targeting rule \"Everyone Else\"."); return(Result <FeatureDecision> .NewResult(new FeatureDecision(everyoneElseRolloutRule, variationResult.ResultObject, FeatureDecision.DECISION_SOURCE_ROLLOUT), reasons)); } } else { var audience = config.GetAudience(everyoneElseRolloutRule.AudienceIds[0]); Logger.Log(LogLevel.DEBUG, $"User \"{userId}\" does not meet the conditions to be in rollout rule for audience \"{audience.Name}\"."); } return(Result <FeatureDecision> .NewResult(null, reasons)); }
public static bool IsFeatureEnabled(FeatureFlag featureFlag) { bool?flagValue = GetFeatureFlagValue(featureFlag); return(flagValue.HasValue && flagValue.Value); }
// For testing only internal FlagBuilder PreconfiguredFlag(FeatureFlag preconfiguredFlag) { _preconfiguredFlag = preconfiguredFlag; return(this); }
public void EnableFeature(FeatureFlag feature) { m_FeatureFlag |= feature; }
public void DisableFeature(FeatureFlag feature) { m_FeatureFlag = m_FeatureFlag & (~feature); }
internal static bool UpsertFlag(IDataStore store, FeatureFlag item) => store.Upsert(DataModel.Features, item.Key, DescriptorOf(item));
public bool HasFeature(FeatureFlag feature) { return((m_FeatureFlag & feature) != 0); }
internal static ItemDescriptor DescriptorOf(FeatureFlag item) => new ItemDescriptor(item.Version, item);
public DataSetBuilder Add(string key, FeatureFlag flag) { _items.Add(new KeyValuePair <string, ItemDescriptor>(key, flag.ToItemDescriptor())); return(this); }
public static string ToJsonString(this FeatureFlag flag, string key) => LdValue.BuildObject().Copy(LdValue.Parse(flag.ToJsonString())).Set("key", key).Build().ToJsonString();
public async Task <Payload <bool> > DeleteFeatureFlag(FeatureFlag featureFlag) { Uri url = new Uri($"api/FeatureFlags/DeleteFeatureFlag", UriKind.Relative); return(await PostMessageItem <bool>(url, featureFlag)); }
public async Task <bool> AddFeatureFlag(FeatureFlag featureFlag) { Uri url = new Uri($"api/FeatureFlags/SaveFeatureFlag", UriKind.Relative); return(await PostMessageItem <bool>(url, featureFlag)); }
protected override void Seed(FeatureFlags.Data.DataContext context) { // This method will be called after migrating to the latest version. // You can use the DbSet<T>.AddOrUpdate() helper extension method // to avoid creating duplicate seed data. E.g. // // context.People.AddOrUpdate( // p => p.FullName, // new Person { FullName = "Andrew Peters" }, // new Person { FullName = "Brice Lambson" }, // new Person { FullName = "Rowan Miller" } // ); // var flag = new FeatureFlag { Key = new Guid("F803A4D0-F7BF-4513-BAAC-EDDB4477BB94"), FlagKeyValue = "Task.Screen", EffectiveDate = DateTime.Now }; var providerFlag = new FeatureFlag { Key = new Guid("3C25EE27-AE91-42BD-9CD4-6F6B579D3B2B"), FlagKeyValue = "Task.Provider", EffectiveDate = DateTime.Now }; context.FeatureFlag.AddOrUpdate(f => f.Key, flag, providerFlag ); var featureRole = new FeatureRole { Key = new Guid("1CC07636-7109-47D8-B647-C12CA1BB3BD3"), RoleName = "TaskTester", EffectiveDate = DateTime.Now }; context.FeatureRole.AddOrUpdate(r => r.Key, featureRole ); var featureRoleUser = new FeatureRoleUser { Key = new Guid("5A9C2523-A7E5-466F-A70D-9593D72CDC63"), UserName = "", Role = featureRole, EffectiveDate = DateTime.Now }; context.FeatureRoleUser.AddOrUpdate(u => u.Key, featureRoleUser ); var featureStateOff = new FeatureState { Key = new Guid("51EF79AF-3F6A-4DCC-AC42-A81DE3ECFE8B"), StateCode = "Off", StateDisplay = "Off", EffectiveDate = DateTime.Now }; var featureStateOn = new FeatureState { Key = new Guid("0AD57AAC-1EB8-467B-B6C3-C4E3860929B5"), StateCode = "On", StateDisplay = "On", EffectiveDate = DateTime.Now }; var featureStatePreview = new FeatureState { Key = new Guid("A09390BA-4D99-4E8A-8A89-E8487663E42F"), StateCode = "Preview", StateDisplay = "Preview", EffectiveDate = DateTime.Now }; context.FeatureState.AddOrUpdate(s => s.Key, featureStateOff, featureStateOn, featureStatePreview ); //var featureStateRole = new FeatureStateRole { Key = new Guid("9C9EC0A5-C8CF-490F-B658-91F8DCC5CF5F"), Flag = flag, RoleUser = featureRoleUser, State = featureStateOff, EffectiveDate = DateTime.Now }; //context.FeatureStateRole.AddOrUpdate(fsr => fsr.Key, // featureStateRole // ); var featureProviderStateRole = new FeatureStateRole { Key = new Guid("F9240688-A3C8-40FF-B6BB-BBCD2A9F2B37"), Flag = providerFlag, RoleUser = featureRoleUser, State = featureStatePreview, EffectiveDate = DateTime.Now }; context.FeatureStateRole.AddOrUpdate(fsr => fsr.Key, featureProviderStateRole ); context.SaveChanges(); }
private void AssertFeatureInStore(FeatureFlag f) { Assert.Equal(f.Version, _featureStore.Get(VersionedDataKind.Features, f.Key).Version); }
public static bool IsFeatureEnabled(int vehicle, FeatureFlag flag) { var state = (FeatureFlag)API.DecorGetInt(vehicle, FeatureFlagsDecor); return((state & flag) == flag); }
private void AssertFeatureInStore(FeatureFlag f) { Assert.Equal(f.Version, _dataStore.Get(DataModel.Features, f.Key).Value.Version); }
public async Task TestProgressiveDisclosure() { // Get your key from https://console.developers.google.com/apis/credentials var apiKey = "AIzaSyCtcFQMgRIUHhSuXggm4BtXT4eZvUrBWN0"; // https://docs.google.com/spreadsheets/d/1KBamVmgEUX-fyogMJ48TT6h2kAMKyWU1uBL5skCGRBM contains the sheetId: var sheetId = "1KBamVmgEUX-fyogMJ48TT6h2kAMKyWU1uBL5skCGRBM"; var sheetName = "MySheet1"; // Has to match the sheet name var googleSheetsStore = new GoogleSheetsKeyValueStore(new InMemoryKeyValueStore(), apiKey, sheetId, sheetName); var testStore = new FeatureFlagStore(new InMemoryKeyValueStore(), googleSheetsStore); IoC.inject.SetSingleton <FeatureFlagManager <FeatureFlag> >(new FeatureFlagManager <FeatureFlag>(testStore)); // Make sure user would normally be included in the rollout: var flagId4 = "MyFlag4"; var flag4 = await FeatureFlagManager <FeatureFlag> .instance.GetFeatureFlag(flagId4); Assert.Equal(flagId4, flag4.id); Assert.Equal(100, flag4.rolloutPercentage); // There is no user progression system setup so the requiredXp value of the feature flag is ignored: Assert.True(await FeatureFlag.IsEnabled(flagId4)); Assert.True(await flag4.IsFeatureUnlocked()); // Setup progression system and check again: var xpSystem = new TestXpSystem(); IoC.inject.SetSingleton <IProgressionSystem <FeatureFlag> >(xpSystem); // Now that there is a progression system Assert.False(await flag4.IsFeatureUnlocked()); Assert.False(await FeatureFlag.IsEnabled(flagId4)); var eventCount = 1000; var store = new MutationObserverKeyValueStore().WithFallbackStore(new InMemoryKeyValueStore()); // Lets assume the users xp correlates with the number of triggered local analytics events: store.onSet = delegate { xpSystem.currentXp++; return(Task.FromResult(true)); }; // Set the store to be the target of the local analytics so that whenever any LocalAnalytics analytics = new LocalAnalytics(store); analytics.createStoreFor = (_) => new InMemoryKeyValueStore().GetTypeAdapter <AppFlowEvent>(); // Setup the AppFlow logic to use LocalAnalytics: AppFlow.AddAppFlowTracker(new AppFlowToStore(analytics)); // Simulate User progression by causing analytics events: for (int i = 0; i < eventCount; i++) { AppFlow.TrackEvent(EventConsts.catMutation, "User did mutation nr " + i); } // Get the analtics store for category "Mutations": var mutationStore = await analytics.GetStoreForCategory(EventConsts.catMutation).GetAll(); Assert.Equal(eventCount, mutationStore.Count()); // All events so far were mutations Assert.True(eventCount <= xpSystem.currentXp); // The user received xp for each mutation Assert.Equal(1000, flag4.requiredXp); // The user needs >= 1000 xp for the feature // Now that the user has more than 1000 xp the condition of the TestXpSystem is met: Assert.True(await flag4.IsFeatureUnlocked()); Assert.True(await FeatureFlag.IsEnabled(flagId4)); }
private void AssertFlagHasAllProperties(FeatureFlag flag) { Assert.Equal("flag-key", flag.Key); Assert.Equal(99, flag.Version); Assert.True(flag.On); Assert.Equal("123", flag.Salt); Assert.Collection(flag.Prerequisites, p => { Assert.Equal("prereqkey", p.Key); Assert.Equal(3, p.Variation); }); Assert.Collection(flag.Targets, t => { Assert.Equal(1, t.Variation); Assert.Equal(ImmutableList.Create("key1", "key2"), t.Values); }); Assert.Collection(flag.Rules, r => { Assert.Equal("id0", r.Id); Assert.True(r.TrackEvents); Assert.Equal(2, r.Variation); Assert.Null(r.Rollout); Assert.Collection(r.Clauses, c => { Assert.Equal(UserAttribute.Name, c.Attribute); Assert.Equal(Operator.In, c.Op); Assert.Equal(ImmutableList.Create(LdValue.Of("Lucy"), LdValue.Of("Mina")), c.Values); Assert.True(c.Negate); }); }, r => { Assert.Equal("id1", r.Id); Assert.False(r.TrackEvents); Assert.Null(r.Variation); Assert.NotNull(r.Rollout); Assert.Collection(r.Rollout.Value.Variations, v => { Assert.Equal(2, v.Variation); Assert.Equal(100000, v.Weight); }); Assert.Equal(UserAttribute.Email, r.Rollout.Value.BucketBy); Assert.Collection(r.Clauses); }); Assert.NotNull(flag.Fallthrough); Assert.Equal(1, flag.Fallthrough.Variation); Assert.Null(flag.Fallthrough.Rollout); Assert.Equal(2, flag.OffVariation); Assert.Equal(ImmutableList.Create(LdValue.Of("a"), LdValue.Of("b"), LdValue.Of("c")), flag.Variations); Assert.True(flag.ClientSide); Assert.True(flag.TrackEvents); Assert.True(flag.TrackEventsFallthrough); Assert.Equal(UnixMillisecondTime.OfMillis(1000), flag.DebugEventsUntilDate); }