public static UsageRule NewFeatureNotUsedXDaysRule(this LocalAnalytics self, string featureId, int days) { return(new UsageRule(UsageRule.FeatureNotUsedXDays) { categoryId = featureId, days = days }.SetupUsing(self)); }
private static async Task AddAFewLocalNewsEvents(InMemoryKeyValueStore onDeviceEventsStore) { var a = new LocalAnalytics(); // Set up a few simple usage rules to generate local news events from if they are true: var appUsed1DayRule = a.NewAppUsedXDaysRule(days: 1); var featureNeverUsedRule = a.NewFeatureNotUsedXTimesRule("feature1", times: 1); var appNotUsedLast5Days = a.NewAppUsedInTheLastXDaysRule(5); var userCameBackRule = a.NewConcatRule(appUsed1DayRule, appNotUsedLast5Days); var url = "https://github.com/cs-util-com/cscore"; var urlText = "Show details.."; if (await appUsed1DayRule.isTrue()) { var n = News.NewLocalNewsEvent("Achievement Unlocked", "You used the app 1 day", url, urlText); await onDeviceEventsStore.Set(n.key, n); } if (await featureNeverUsedRule.isTrue()) { var n = News.NewLocalNewsEvent("Did you know you can do feature1?", "Feature 1 is the best", url, urlText); await onDeviceEventsStore.Set(n.key, n); } if (await userCameBackRule.isTrue()) { var n = News.NewLocalNewsEvent("You did not use the app for a while", "How dare you", url, urlText); await onDeviceEventsStore.Set(n.key, n); } }
public async Task TestLocalAnalytics() { int eventCount = 10000; // Create a LocalAnalytics instance that uses only memory stores for testing: LocalAnalytics localAnalytics = new LocalAnalytics(new InMemoryKeyValueStore()); localAnalytics.createStoreFor = (_) => new InMemoryKeyValueStore().GetTypeAdapter <AppFlowEvent>(); // Pass this local analytics system to the app flow impl. as the target store: AppFlowToStore appFlow = new AppFlowToStore(localAnalytics); await TestAppFlowWithStore(eventCount, appFlow); // Run the tests // Get the store that contains only the events of a specific category: var catMethodStore = localAnalytics.GetStoreForCategory(EventConsts.catMethod); { // Check that all events so far are of the method category: var all = await localAnalytics.GetAll(); var allForCat = await catMethodStore.GetAll(); Assert.Equal(all.Count(), allForCat.Count()); } { // Add an event of a different category and check that the numbers again: appFlow.TrackEvent(EventConsts.catUi, "Some UI event"); var all = await localAnalytics.GetAll(); var allForCat = await catMethodStore.GetAll(); Assert.Equal(all.Count(), allForCat.Count() + 1); var catUiStore = localAnalytics.GetStoreForCategory(EventConsts.catUi); Assert.Single(await catUiStore.GetAll()); } }
public static UsageRule NewConcatRule(this LocalAnalytics self, params UsageRule[] andRules) { return(new UsageRule(UsageRule.ConcatRule) { andRules = andRules.ToList() }.SetupUsing(self)); }
public static UsageRule NewFeatureNotUsedXTimesRule(this LocalAnalytics self, string featureId, int times) { return(new UsageRule(UsageRule.FeatureNotUsedXTimes) { categoryId = featureId, timesUsed = times }.SetupUsing(self)); }
public ProgressionSystem(LocalAnalytics analytics, FeatureFlagManager <T> featureFlagManager) { // Make sure the FeatureFlag system was set up too: AssertV2.IsNotNull(FeatureFlagManager <T> .instance, "FeatureFlagManager.instance"); this.analytics = analytics; this.featureFlagManager = featureFlagManager; }
public static UsageRule NewNotificationMinXDaysOldRule(this LocalAnalytics self, string notificationId, int days) { return(new UsageRule(UsageRule.NotificationMinXDaysOld) { categoryId = notificationId, days = days }.SetupUsing(self)); }
public static UsageRule NewAppNotUsedXDaysRule(this LocalAnalytics self, int days) { return(new UsageRule(UsageRule.AppNotUsedXDays) { days = days }.SetupUsing(self)); }
private static LocalAnalytics CreateLocalAnalyticsSystem() { LocalAnalytics analytics = new LocalAnalytics(new InMemoryKeyValueStore()); analytics.createStoreFor = (_) => new InMemoryKeyValueStore().GetTypeAdapter <AppFlowEvent>(); // Setup the AppFlow logic to use the LocalAnalytics system: AppFlow.AddAppFlowTracker(new AppFlowToStore(analytics)); return(analytics); }
async Task AssertFeatureUsageDetected(LocalAnalytics analytics, string featureId, int expectedCount) { var featureEventStore = analytics.categoryStores[featureId]; var allFeatureEvents = await featureEventStore.GetAll(); Assert.Equal(expectedCount, allFeatureEvents.Count()); var allStartEvents = allFeatureEvents.Filter(x => x.action == EventConsts.START); Assert.Equal(expectedCount, allStartEvents.Count()); }
public static void Setup(DefaultFeatureFlagStore featureFlagStore) { IoC.inject.SetSingleton(new FeatureFlagManager(featureFlagStore)); LocalAnalytics analytics = new LocalAnalytics(); AppFlow.AddAppFlowTracker(new AppFlowToStore(analytics)); var xpSystem = new DefaultProgressionSystem(analytics); IoC.inject.SetSingleton <ProgressionSystem>(xpSystem); }
// Cleanup from previous tests (needed because persistance to disc is used): private static async Task CleanupFilesFromTest(LocalAnalytics analytics, ProgressionSystem <FeatureFlag> xpSystem) { // First trigger 1 event for each relevant catory to load the category stores: foreach (var category in xpSystem.xpFactors.Keys) { AppFlow.TrackEvent(category, "Dummy Event"); } await analytics.RemoveAll(); // Then clear all stores Assert.Empty(await analytics.GetAllKeys()); // Now the main store should be emtpy }
async Task TestAllRules1(LocalAnalytics analytics, string featureId) { var daysUsed = 20; var timesUsed = 200; UsageRule featureUsedXDays = analytics.NewFeatureUsedXDaysRule(featureId, daysUsed); Assert.False(await featureUsedXDays.isTrue()); Assert.False(await featureUsedXDays.IsFeatureUsedXDays(analytics)); // Used by .isTrue UsageRule featureNotUsedXDays = analytics.NewFeatureNotUsedXDaysRule(featureId, daysUsed); Assert.True(await featureNotUsedXDays.isTrue()); UsageRule appNotUsedXDays = analytics.NewAppNotUsedXDaysRule(daysUsed); Assert.True(await appNotUsedXDays.isTrue()); UsageRule featureNotUsedXTimes = analytics.NewFeatureNotUsedXTimesRule(featureId, timesUsed); Assert.True(await featureNotUsedXTimes.isTrue()); UsageRule appUsedInTheLastXDays = analytics.NewAppUsedInTheLastXDaysRule(daysUsed); Assert.True(await appUsedInTheLastXDays.isTrue()); UsageRule appNotUsedInTheLastXDays = analytics.NewAppNotUsedInTheLastXDaysRule(daysUsed); Assert.False(await appNotUsedInTheLastXDays.isTrue()); UsageRule featureUsedInTheLastXDays = analytics.NewFeatureUsedInTheLastXDaysRule(featureId, daysUsed); Assert.True(await featureUsedInTheLastXDays.isTrue()); { // Compose a more complex usage rule out of multiple rules: UsageRule appUsedXDays = analytics.NewAppUsedXDaysRule(daysUsed); Assert.False(await appUsedXDays.isTrue()); UsageRule featureUsedXTimes = analytics.NewFeatureUsedXTimesRule(featureId, timesUsed); Assert.False(await featureUsedXTimes.isTrue()); UsageRule featureNotUsedInTheLastXDays = analytics.NewFeatureNotUsedInTheLastXDaysRule(featureId, daysUsed); Assert.False(await featureNotUsedInTheLastXDays.isTrue()); UsageRule featureNotUsedAnymoreRule = analytics.NewConcatRule( appUsedXDays, featureUsedXTimes, featureNotUsedInTheLastXDays ); Assert.False(await featureNotUsedAnymoreRule.isTrue()); UsageRule clone = featureNotUsedAnymoreRule.DeepCopyViaJson(); clone.SetupUsing(analytics); Assert.False(await clone.isTrue()); } }
private static async Task <ProgressionSystem <FeatureFlag> > NewInMemoryTestXpSystem(string apiKey, string sheetId, string sheetName) { var cachedFlags = new InMemoryKeyValueStore(); var googleSheetsStore = new GoogleSheetsKeyValueStore(cachedFlags, apiKey, sheetId, sheetName); var cachedFlagsLocalData = new InMemoryKeyValueStore(); var analytics = new LocalAnalytics(new InMemoryKeyValueStore()); analytics.createStoreFor = (_ => new InMemoryKeyValueStore().GetTypeAdapter <AppFlowEvent>()); var featureFlagStore = new FeatureFlagStore(cachedFlagsLocalData, googleSheetsStore); return(await DefaultProgressionSystem.Setup(featureFlagStore, analytics)); }
public static UsageRule SetupUsing(this UsageRule self, LocalAnalytics analytics) { self.isTrue = async() => { switch (self.ruleType) { case UsageRule.AppUsedXDays: return(await self.IsAppUsedXDays(analytics)); case UsageRule.AppNotUsedXDays: return(!await self.IsAppUsedXDays(analytics)); case UsageRule.AppUsedInTheLastXDays: return(self.IsAppUsedInTheLastXDays()); case UsageRule.AppNotUsedInTheLastXDays: return(!self.IsAppUsedInTheLastXDays()); case UsageRule.FeatureUsedInTheLastXDays: return(await self.IsFeatureUsedInTheLastXDays(analytics)); case UsageRule.FeatureNotUsedInTheLastXDays: return(!await self.IsFeatureUsedInTheLastXDays(analytics)); case UsageRule.FeatureUsedXDays: return(await self.IsFeatureUsedXDays(analytics)); case UsageRule.FeatureNotUsedXDays: return(!await self.IsFeatureUsedXDays(analytics)); case UsageRule.FeatureUsedXTimes: return(await self.IsFeatureUsedXTimes(analytics)); case UsageRule.FeatureNotUsedXTimes: return(!await self.IsFeatureUsedXTimes(analytics)); case UsageRule.NotificationMinXDaysOld: return(await self.IsNotificationMinXDaysOld(analytics)); case UsageRule.ConcatRule: foreach (var rule in self.andRules) { if (rule.isTrue == null) { rule.SetupUsing(analytics); } if (!await rule.isTrue()) { return(false); } } return(true); default: Log.e("Unknown ruleType: " + self.ruleType); return(false); } }; return(self); }
public async Task TestDefaultProgressionSystem() { // 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 ffm = new FeatureFlagManager <FeatureFlag>(new FeatureFlagStore(new InMemoryKeyValueStore(), googleSheetsStore)); IoC.inject.SetSingleton <FeatureFlagManager <FeatureFlag> >(ffm); LocalAnalytics analytics = new LocalAnalytics(); AppFlow.AddAppFlowTracker(new AppFlowToStore(analytics)); var xpSystem = new ProgressionSystem <FeatureFlag>(analytics, ffm); IoC.inject.SetSingleton <IProgressionSystem <FeatureFlag> >(xpSystem); await CleanupFilesFromTest(analytics, xpSystem); // Simulate User progression by causing analytics events: var eventCount = 1000; for (int i = 0; i < eventCount; i++) { AppFlow.TrackEvent(EventConsts.catMutation, "User did mutation nr " + i); } var flagId4 = "MyFlag4"; var flag4 = await FeatureFlagManager <FeatureFlag> .instance.GetFeatureFlag(flagId4); Assert.Equal(1000, flag4.requiredXp); // The user needs >= 1000 XP for the feature // Now that the user has 1000 XP the condition of the TestXpSystem is met: Assert.True(await FeatureFlag.IsEnabled(flagId4)); // The number of mutation events: Assert.Equal(eventCount, xpSystem.cachedCategoryCounts[EventConsts.catMutation]); // Since there are only mutation events the XP is equal to the factor*event count: Assert.Equal(await xpSystem.GetLatestXp(), eventCount * xpSystem.xpFactors[EventConsts.catMutation]); await CleanupFilesFromTest(analytics, xpSystem); }
public static async Task <ProgressionSystem <FeatureFlag> > Setup(KeyValueStoreTypeAdapter <FeatureFlag> featureFlagStore, LocalAnalytics analytics) { var ffm = new FeatureFlagManager <FeatureFlag>(featureFlagStore); IoC.inject.SetSingleton(ffm); AppFlow.AddAppFlowTracker(new AppFlowToStore(analytics).WithBasicTrackingActive()); var xpSystem = new ProgressionSystem <FeatureFlag>(analytics, ffm); IoC.inject.SetSingleton <IProgressionSystem <FeatureFlag> >(xpSystem); await xpSystem.UpdateCurrentCategoryCounts(); return(xpSystem); }
public static async Task <bool> IsFeatureUsedXDays(this UsageRule self, LocalAnalytics analytics) { var allEvents = await analytics.GetAllEventsForCategory(self.categoryId); return(allEvents.GroupByDay().Count() >= self.days); }
public static async Task <bool> IsNotificationMinXDaysOld(this UsageRule self, LocalAnalytics analytics) { var allEvents = await analytics.GetAllEventsForCategory(EventConsts.catUsage); var showEvents = allEvents.Filter(x => x.action == EventConsts.SHOW + "_" + self.categoryId); if (showEvents.IsNullOrEmpty()) { return(false); } DateTime firstShownEvent = showEvents.First().GetDateTimeUtc(); TimeSpan firstShownVsNow = DateTimeV2.UtcNow - firstShownEvent; return(firstShownVsNow.Days >= self.days); }
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)); }
public static async Task <bool> IsAppUsedXDays(this UsageRule self, LocalAnalytics analytics) { var allEvents = await analytics.GetAll(); return(allEvents.GroupByDay().Count() >= self.days); }
public DefaultProgressionSystem(LocalAnalytics analytics) { // Make sure the FeatureFlag system was set up too: AssertV2.NotNull(FeatureFlagManager.instance, "FeatureFlagManager.instance"); this.analytics = analytics; }
private static async Task <IEnumerable <AppFlowEvent> > GetAllEventsForCategory(this LocalAnalytics self, string categoryId) { if (!self.categoryStores.ContainsKey(categoryId)) { return(Enumerable.Empty <AppFlowEvent>()); } return(await self.categoryStores[categoryId].GetAll()); }
public static async Task <bool> IsFeatureUsedInTheLastXDays(this UsageRule self, LocalAnalytics analytics) { var allEvents = await analytics.GetAllEventsForCategory(self.categoryId); if (allEvents.IsNullOrEmpty()) { return(false); } DateTime lastEvent = allEvents.Last().GetDateTimeUtc(); TimeSpan lastEventVsNow = DateTimeV2.UtcNow - lastEvent; return(lastEventVsNow.Days <= self.days); }
public static async Task <IEnumerable <UsageRule> > GetRulesInitialized(this KeyValueStoreTypeAdapter <UsageRule> self, LocalAnalytics analytics) { var rules = await self.GetAll(); foreach (var rule in rules) { if (!rule.concatRuleIds.IsNullOrEmpty()) { rule.andRules = new List <UsageRule>(); foreach (var id in rule.concatRuleIds) { rule.andRules.Add(await self.Get(id, null)); } } if (rule.isTrue == null) { rule.SetupUsing(analytics); } } return(rules); }
public static async Task <bool> IsFeatureUsedXTimes(this UsageRule self, LocalAnalytics analytics) { var startEvents = await analytics.GetStartEvents(self.categoryId); return(startEvents.Count() >= self.timesUsed.Value); }
public static async Task <IEnumerable <AppFlowEvent> > GetStartEvents(this LocalAnalytics self, string categoryId) { var allEvents = await self.GetAllEventsForCategory(categoryId); return(allEvents.Filter(x => x.action == EventConsts.START)); }