diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2017-11-20 10:33:36 +0100 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2017-11-22 11:45:12 +0000 |
commit | be59a35641616a4cf23c4a13fa0632624b021c1b (patch) | |
tree | 9da183258bdf9cc413f7562079d25ace6955467f /chromium/components/feature_engagement | |
parent | d702e4b6a64574e97fc7df8fe3238cde70242080 (diff) | |
download | qtwebengine-chromium-be59a35641616a4cf23c4a13fa0632624b021c1b.tar.gz |
BASELINE: Update Chromium to 62.0.3202.101
Change-Id: I2d5eca8117600df6d331f6166ab24d943d9814ac
Reviewed-by: Alexandru Croitor <alexandru.croitor@qt.io>
Diffstat (limited to 'chromium/components/feature_engagement')
85 files changed, 9349 insertions, 0 deletions
diff --git a/chromium/components/feature_engagement/BUILD.gn b/chromium/components/feature_engagement/BUILD.gn new file mode 100644 index 00000000000..c79a97afc70 --- /dev/null +++ b/chromium/components/feature_engagement/BUILD.gn @@ -0,0 +1,47 @@ +# Copyright 2017 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if (is_android) { + import("//build/config/android/config.gni") + import("//build/config/android/rules.gni") +} + +group("feature_engagement") { + public_deps = [ + "//components/feature_engagement/public", + ] + + deps = [ + "//components/feature_engagement/internal", + ] +} + +group("unit_tests") { + testonly = true + + deps = [ + "//components/feature_engagement/internal:unit_tests", + ] + + data_deps = [ + ":components_unittests_gtest_filter", + ] +} + +source_set("components_unittests_gtest_filter") { + testonly = true + + data = [ + "components_unittests.filter", + ] +} + +if (is_android) { + java_group("feature_engagement_java") { + deps = [ + "//components/feature_engagement/internal:internal_java", + "//components/feature_engagement/public:public_java", + ] + } +} diff --git a/chromium/components/feature_engagement/DEPS b/chromium/components/feature_engagement/DEPS new file mode 100644 index 00000000000..48edfa45a2c --- /dev/null +++ b/chromium/components/feature_engagement/DEPS @@ -0,0 +1,7 @@ +include_rules = [ + "-content", + "+jni", + "+components/flags_ui", + "+components/keyed_service", + "+components/leveldb_proto", +] diff --git a/chromium/components/feature_engagement/OWNERS b/chromium/components/feature_engagement/OWNERS new file mode 100644 index 00000000000..f3889057fbe --- /dev/null +++ b/chromium/components/feature_engagement/OWNERS @@ -0,0 +1,5 @@ +dtrainor@chromium.org +nyquist@chromium.org + +# COMPONENT: Internals>FeatureEngagement + diff --git a/chromium/components/feature_engagement/README.md b/chromium/components/feature_engagement/README.md new file mode 100644 index 00000000000..f632bc19184 --- /dev/null +++ b/chromium/components/feature_engagement/README.md @@ -0,0 +1,489 @@ +# Feature Engagement + +The Feature Engagement component provides a client-side backend for displaying +feature enlightenment or in-product help (IPH) with a clean and easy to use API +to be consumed by the UI frontend. The backend behaves as a black box and takes +input about user behavior. Whenever the frontend gives a trigger signal that +in-product help could be displayed, the backend will provide an answer to +whether it is appropriate to show it or not. + +[TOC] + +## Objectives + +We often add new features, but some are hard to find. Both new and old features +could benefit from being surfaced for users that we believe would be the ones +who benefit the most. This has lead to the effort of providing direct in-product +help to our end users that should be extremely context aware to maximize the +value of the new information. + +Conceptually one could implement tracking whether In-Product Help should be +displayed or not through a single preference for whether it has been shown +before. However, that leads to a few issues that this component tries to solve: + +* Make showing In-Product Help context aware. + * If a user is continuously using a feature, there is no reason for Chrome + to display In-Product Help for it. + * Other events might be required to have happened first that would make it + more likely that the end user would be surprised and delighted when the + In-Product Help in fact does show up. + * Example: Having seen the Chrome offline dino 10 times the last week, + the user might be happier if they are informed that they can + download web pages exactly as a page successfully loads. +* Tackle interactions between different In-Product Help features. + * If other In-Product Help has been shown within the current session, we + might not want to show a different one. + * Whether we have shown a particular In-Product Help or not might be a + precondition for whether we should show different one. +* Users should be able to use try out a feature on their own for some time + before they see help. + * We should show In-Product Help only if they don't seem use it, but we + believe it would be helpful to them. +* Share the same statistics framework across all of Chrome. + * Sharing a framework within Chrome makes it easier to track statistics + and share queries about In-Product Help in a common way. +* Make it simpler to add new In-Product Help for developers, but still + enabling them to have a complex decision tree for when to show it. + +## Overview + +Each In-Product Help is called a feature in this documentation. Every feature +will have a few important things that are tracked, particularly whether the +in-product help has been displayed, whether the feature the IPH highlights has +been used and whether any required preconditions have been met. All of these are +tracked within **daily buckets**. This tracking is done only +**locally on the device** itself. + +The client-side backend is feature agnostic and has no special logic for any +specific features, but instead provides a generic API and uses the Chrome +Variations framework to control how often IPH should be shown for end users. It +does this by setting thresholds in the experiment params and compare these +numbers to the local state. + +Whenever the triggering condition for possibly showing IPH happens, the frontend +asks the backend whether it should display the IPH. The backend then +compares the current local state with the experiment params to see if they are +within the given thresholds. If they are, the frontend is informed that it +should display the IPH. The backend does not display any UI. + +To ensure that there are not multiple IPHs displayed at the same time, the +frontend also needs to inform the backend whenever the IPH has been dismissed. + +In addition, since each feature might have preconditions that must be met within +the time window configured for the experiment, the frontend needs to inform the +backend whenever such events happen. + +To ensure that it is possible to use whether a feature has been used or +not as input to the algorithm to decide whether to show IPH and for tracking +purposes, the frontend needs to inform whenever the feature has been used. + +Lastly, some preconditions might require something to never have happened. +The first time a user has IPH available, that will typically be true, since +the event was just started being tracked. Therefore, the first time the +Chrome Variations experiment is made available to the user, the date is tracked +so it can be used to require that the IPH must have been available for at least +`N` days. + +The backend will track all the state in-memory and flush it to disk when +necessary to ensure the data is consistent across restarts of the application. +The time window for how long this data is stored is configured server-side. + +All of the local tracking of data will happen per Chrome user profile, but +everything is configured on the server side. + +## Developing a new In-Product Help Feature + +You need to do the following things to enable your feature, all described in +detail below. + +* [Declare your feature](#Declaring-your-feature) and make it available to the + `feature_engagement::Tracker`. +* [Start using the `feature_engagement::Tracker` class](#Using-the-feature_engagement_Tracker) + by notifying about events, and checking whether In-Product Help should be + displayed. +* [Configure UMA](#Configuring-UMA). + +### Declaring your feature + +You need to create a `base::Feature` that represents your In-Product Help +feature, that enables the whole feature to be controlled server side. +The name should be on the form: + +1. `kIPH` prefix +1. Your unique CamelCased name, for example `MyFun`. +1. `Feature` suffix. + +The name member of the `base::Feature` struct should match the constant name, +and be on the form: + +1. `IPH_` prefix +1. Your unique CamelCased name, for example `MyFun`. + +There are also a few more places where the feature should be added, so overall +you would have to add it to the following places: + +* `//components/feature_engagement/public/feature_constants.cc`: + + ```c++ + const base::Feature kIPHMyFunFeature{"IPH_MyFun", + base::FEATURE_DISABLED_BY_DEFAULT}; + ``` + +* `//components/feature_engagement/public/feature_constants.h`: + + ```c++ + extern const base::Feature kIPHMyFunFeature; + ``` + +* `//components/feature_engagement/public/feature_list.cc`: + * Add to `const base::Feature* kAllFeatures[]`. +* `//components/feature_engagement/public/feature_list.h`: + * `DEFINE_VARIATION_PARAM(kIPHMyFunFeature, "IPH_MyFun");` + * `VARIATION_ENTRY(kIPHMyFunFeature)` + +If the feature will also be used from Java, also add it to: +`org.chromium.components.feature_engagement.FeatureConstants` as a +`String` constant. + +### Using the feature_engagement::Tracker + +To retrieve the `feature_engagement::Tracker` you need to use your platform +specific way for how to retrieve a `KeyedService`. For example for desktop +platforms and Android, you can use the `feature_engagement::TrackerFactory` in +`//chrome/browser/feature_engagement/tracker_factory.h` +to retrieve it from the `Profile` or `BrowserContext`: + +```c++ +feature_engagement::Tracker* tracker = + feature_engagement::TrackerFactory::GetForBrowserContext(profile); +``` + +That service can be first of all used to notify the backend about events: + +```c++ +tracker->NotifyEvent("your_event_name"); +``` + +In addition, it can tell you whether it is a good time to trigger the help UI: + +```c++ +bool trigger_help_ui = + tracker->ShouldTriggerHelpUI(feature_engagement::kIPHMyFunFeature); +if (trigger_help_ui) { + // Show IPH UI. +} +``` + +If `feature_engagement::Tracker::ShouldTriggerHelpUI` return `true` you must +display the In-Product Help, as it will be tracked as if you showed it. In +addition you are required to inform when the feature has been dismissed: + +```c++ +tracker->Dismissed(feature_engagement::kIPHMyFunFeature); +``` + +#### Inspecting whether IPH has already been triggered for a feature + +Sometimes additional tracking is required to figure out if in-product help for a +particular feature should be shown, and sometimes this is costly. If the +in-product help has already been shown for that feature, it might not be +necessary any more to do the additional tracking of state. + +To check if the triggering condition has already been fulfilled (i.e. can not +currently be triggered again), you can call: + +```c++ +// TriggerState is { HAS_BEEN_DISPLAYED, HAS_NOT_BEEN_DISPLAYED, NOT_READY }. +Tracker::TriggerState trigger_state = + GetTriggerState(feature_engagement::kIPHMyFunFeature); +``` + +Inspecting this state requires the Tracker to already have been initialized, +else `NOT_READY` is always returned. See `IsInitialized()` and +`AddOnInitializedCallback(...)` for how to ensure the call to this is delayed. + +##### A note about TriggerState naming + +Typically, the `FeatureConfig` (see below) for any particular in-product help +requires the configuration for `event_trigger` to have a comparator value of +`==0`, i.e. that it is a requirement that the particular in-product help has +never been shown within the search window. The values of the `TriggerState` enum +reflects this typical usage, whereas technically, this is the correct +interpretation of the states: + +* `HAS_BEEN_DISPLAYED`: `event_trigger` condition is NOT met and in-product + help will not be displayed if `Tracker` is asked. +* `HAS_NOT_BEEN_DISPLAYED`: `event_trigger` condition is met and in-product + help might be displayed if `Tracker` is asked. +* `NOT_READY`: `Tracker` not fully initialized yet, so it is unable to + inspect the state. + +### Configuring UMA + +To enable UMA tracking, you need to make the following changes to the metrics +configuration: + +1. Add feature to the histogram suffix `IPHFeatures` in: + `//tools/metrics/histograms/histograms.xml`. + * The suffix must match the `base::Feature` `name` member of your feature. +1. Add feature to the actions file (actions do not support automatic suffixes): + `//tools/metrics/actions/actions.xml`. + * The suffix must match the `base::Feature` `name` member. + * Use an old example to ensure you configure and describe it correctly. + * For `IPH_MyFunFeature` it would look like this: + * `<action name="InProductHelp.NotifyEvent.IPH_MyFunFeature">` + * `<action name="InProductHelp.NotifyUsedEvent.IPH_MyFunFeature">` + * `<action name="InProductHelp.ShouldTriggerHelpUI.IPH_MyFunFeature">` + * `<action name="InProductHelp.ShouldTriggerHelpUIResult.NotTriggered.IPH_MyFunFeature">` + * `<action name="InProductHelp.ShouldTriggerHelpUIResult.Triggered.IPH_MyFunFeature">` + +## Demo mode + +The feature_engagement::Tracker supports a special demo mode, which enables a +developer or testers to see how the UI looks like without using Chrome +Variations configuration. + +The demo mode behaves differently than the code used in production where the +chrome Variations configuration is used. Instead, it has only a few rules: + +* Event model must be ready (happens early). +* No other features must be showing at the moment. +* The given feature must not have been shown before in the current session. + +This basically leads to each selected IPH feature to be displayed once. The +triggering condition code path must of course be triggered to display the IPH. + +How to select a feature or features is described below. + +### Enabling all In-Product Help features in demo-mode + +1. Go to chrome://flags +1. Find "In-Product Help Demo Mode" (#in-product-help-demo-mode-choice) +1. Select "Enabled" +1. Restart Chrome + +### Enabling a single In-Product Help feature in demo-mode + +1. Go to chrome://flags +1. Find “In-Product Help Demo Mode” (#enable-iph-demo-choice) +1. Select the feature you want with the "Enabled " prefix, for example for + `IPH_MyFunFeature` you would select: + * Enabled IPH_MyFunFeature +1. Restart Chrome + +## Using Chrome Variations + +Each In-Product Help feature must have its own feature configuration +[FeatureConfig](#FeatureConfig), which has 4 required configuration items that +must be set, and then there can be an arbitrary number of additional +preconditions (but typically on the order of 0-5). + +The data types are listed below. + +### FeatureConfig + +Format: + +``` +{ + "availability": "{Comparator}", + "session_rate": "{Comparator}", + "event_used": "{EventConfig}", + "event_trigger": "{EventConfig}", + "event_???": "{EventConfig}", + "x_???": "..." + } +``` + +The `FeatureConfig` fields `availability`, `session_rate`, `event_used` and +`event_trigger` are required, and there can be an arbitrary amount of other +`event_???` entries. + +* `availability` + * For how long must an in-product help experiment have been available to + the end user. + * The value of the `Comparator` is in a number of days. +* `session_rate` + * How many other in-product help have been displayed within the current + end user session. + * The value of the `Comparator` is a count of total In-Product Help + displayed in the current end user session. +* `event_used` + * Relates to what the in-product help wants to highlight, i.e. teach the + user about and increase usage of. + * This is typically the action that the In-Product Help should stimulate + usage of. + * Special UMA is tracked for this. +* `event_trigger` + * Relates to the times in-product help is triggered. + * Special UMA is tracked for this. +* `event_???` + * Similar to the other `event_` items, but for all other preconditions + that must have been met. + * Name must match `/^event_[a-zA-Z0-9-_]+$/` and not be `event_used` or + `event_trigger`. +* `x_???` + * Any parameter starting with `x_` is ignored by the feature engagement + tracker. + * A typical use case for this would be if there are multiple experiments + for the same in-product help, and you want to specify different strings + to use in each of them, such as: + + ``` + "x_promo_string": "IDS_MYFUN_PROMO_2" + ``` + + * Failing to use an `x_`-prefix for parameters unrelated to the + `FeatureConfig` will end up being recorded as `FAILURE_UNKNOWN_KEY` in + the `InProductHelp.Config.ParsingEvent` histogram. + +**Examples** + +``` +{ + "availability": ">=30", + "session_rate": "<1", + "event_used": "name:download_home_opened;comparator:any;window:90;storage:360", + "event_trigger": "name:download_home_iph_trigger;comparator:any;window:90;storage:360", + "event_1": "name:download_completed;comparator:>=1;window:120;storage:180" +} +``` + +### EventConfig + +Format: ```name:{std::string};comparator:{COMPARATOR};window:{uint32_t};storage:{uint32_t}``` + +The EventConfig is a semi-colon separate data structure with 4 key-value pairs, +all described below: + +* `name` + * The name (unique identifier) of the event. + * Must match what is used in client side code. + * Must only contain alphanumeric, dash and underscore. + * Specifically must match this regex: `/^[a-zA-Z0-9-_]+$/` + * Value client side data type: std::string +* `comparator` + * The comparator for the event. See [Comparator](#Comparator) below. +* `window` + * Search for this occurrences of the event within this window. + * The value must be given as a number of days. + * Value client side data type: uint32_t +* `storage` + * Store client side data related to events for this event minimum this + long. + * The value must be given as a number of days. + * The value should not exceed 10 years (3650 days). + * Value client side data type: uint32_t + * Whenever a particular event is used by multiple features, the maximum + value of all `storage` is used as the storage window. + +**Examples** + +``` +name:user_opened_app_menu;comparator:==0;window:14;storage:90 +name:user_has_seen_dino;comparator:>=5;window:30;storage:360 +name:user_has_seen_wifi;comparator:>=1;window:30;storage:180 +``` + +### Comparator + +Format: ```{COMPARATOR}[value]``` + +The following comparators are allowed: + +* `<` less than +* `>` greater than +* `<=` less than or equal +* `>=` greater than or equal +* `==` equal +* `!=` not equal +* `any` always true (no value allowed) + +Other than `any`, all comparators require a value. + +**Examples** + +``` +>=10 +==0 +any +<15 +``` + +### Using Chrome Variations at runtime + +It is possible to test the whole backend from parsing the configuration, +to ensuring that help triggers at the correct time. To do that +you need to provide a JSON configuration file, that is then +parsed to become command line arguments for Chrome, and after +that you can start Chrome and verify that it behaves correctly. + +1. Create a file which describes the configuration you are planning + on testing with, and store it. In the following example, store the + file `DownloadStudy.json`: + + ```javascript + { + "DownloadStudy": [ + { + "platforms": ["android"], + "experiments": [ + { + "name": "DownloadExperiment", + "params": { + "availability": ">=30", + "session_rate": "<1", + "event_used": "name:download_home_opened;comparator:any;window:90;storage:360", + "event_trigger": "name:download_home_iph_trigger;comparator:any;window:90;storage:360", + "event_1": "name:download_completed;comparator:>=1;window:120;storage:180" + }, + "enable_features": ["IPH_DownloadHome"], + "disable_features": [] + } + ] + } + ] + } + ``` + +1. Use the field trial utility to convert the JSON configuration to command + line arguments: + + ```bash + python ./tools/variations/fieldtrial_util.py DownloadStudy.json android shell_cmd + ``` + +1. Pass the command line along to the binary you are planning on running or the + command line utility for the Android platform. + + For the target `chrome_public_apk` it would be: + + ```bash + ./build/android/adb_chrome_public_command_line "--force-fieldtrials=DownloadStudy/DownloadExperiment" "--force-fieldtrial-params=DownloadStudy.DownloadExperiment:availability/>=30/event_1/name%3Adownload_completed;comparator%3A>=1;window%3A120;storage%3A180/event_trigger/name%3Adownload_home_iph_trigger;comparator%3Aany;window%3A90;storage%3A360/event_used/name%3Adownload_home_opened;comparator%3Aany;window%3A90;storage%3A360/session_rate/<1" "--enable-features=IPH_DownloadHome<DownloadStudy" + ``` + +### Printf debugging + +Several parts of the feature engagement tracker has some debug logging +available. To see if the current checked in code covers your needs, try starting +a debug build of chrome with the following command line arguments: + +```bash +--vmodule=tracker_impl*=2,event_model_impl*=2,persistent_availability_store*=2,chrome_variations_configuration*=3 +``` + +## Development of `//components/feature_engagement` + +### Testing + +To compile and run tests, assuming the product out directory is `out/Debug`, +use: + +```bash +ninja -C out/Debug components_unittests ; +./out/Debug/components_unittests \ + --test-launcher-filter-file=components/feature_engagement/components_unittests.filter +``` + +When adding new test suites, also remember to add the suite to the filter file: +`//components/feature_engagement/components_unittests.filter`. diff --git a/chromium/components/feature_engagement/components_unittests.filter b/chromium/components/feature_engagement/components_unittests.filter new file mode 100644 index 00000000000..ad53cb611c4 --- /dev/null +++ b/chromium/components/feature_engagement/components_unittests.filter @@ -0,0 +1,22 @@ +AvailabilityModelImplTest.* +ChromeVariationsConfigurationTest.* +ComparatorTest.* +ConditionValidatorResultTest.* +EditableConfigurationTest.* +EventModelImplTest.* +FailingAvailabilityModelInitTrackerImplTest.* +FailingStoreInitTrackerImplTest.* +FeatureConfigConditionValidatorTest.* +FeatureConfigEventStorageValidatorTest.* +TrackerImplTest.* +InitAwareEventModelTest.* +InMemoryEventStoreTest.* +LoadFailingEventModelImplTest.* +NeverAvailabilityModelTest.* +NeverConditionValidatorTest.* +NeverEventStorageValidatorTest.* +OnceConditionValidatorTest.* +PersistentAvailabilityStoreTest.* +PersistentEventStoreTest.* +SingleInvalidConfigurationTest.* +SystemTimeProviderTest.* diff --git a/chromium/components/feature_engagement/internal/BUILD.gn b/chromium/components/feature_engagement/internal/BUILD.gn new file mode 100644 index 00000000000..3e5b25b6521 --- /dev/null +++ b/chromium/components/feature_engagement/internal/BUILD.gn @@ -0,0 +1,145 @@ +# Copyright 2017 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if (is_android) { + import("//build/config/android/config.gni") + import("//build/config/android/rules.gni") +} + +static_library("internal") { + visibility = [ + ":*", + "//components/feature_engagement", + "//components/feature_engagement/test:test_support", + ] + + sources = [ + "availability_model.h", + "availability_model_impl.cc", + "availability_model_impl.h", + "chrome_variations_configuration.cc", + "chrome_variations_configuration.h", + "condition_validator.cc", + "condition_validator.h", + "configuration.cc", + "configuration.h", + "editable_configuration.cc", + "editable_configuration.h", + "event_model.h", + "event_model_impl.cc", + "event_model_impl.h", + "event_storage_validator.h", + "event_store.h", + "feature_config_condition_validator.cc", + "feature_config_condition_validator.h", + "feature_config_event_storage_validator.cc", + "feature_config_event_storage_validator.h", + "in_memory_event_store.cc", + "in_memory_event_store.h", + "init_aware_event_model.cc", + "init_aware_event_model.h", + "never_availability_model.cc", + "never_availability_model.h", + "never_condition_validator.cc", + "never_condition_validator.h", + "never_event_storage_validator.cc", + "never_event_storage_validator.h", + "once_condition_validator.cc", + "once_condition_validator.h", + "persistent_availability_store.cc", + "persistent_availability_store.h", + "persistent_event_store.cc", + "persistent_event_store.h", + "single_invalid_configuration.cc", + "single_invalid_configuration.h", + "stats.cc", + "stats.h", + "system_time_provider.cc", + "system_time_provider.h", + "time_provider.h", + "tracker_impl.cc", + "tracker_impl.h", + ] + + public_deps = [ + "//components/feature_engagement/internal/proto", + ] + + deps = [ + "//base", + "//components/feature_engagement/public", + "//components/keyed_service/core", + "//components/leveldb_proto", + ] + + if (is_android) { + sources += [ + "android/tracker_impl_android.cc", + "android/tracker_impl_android.h", + ] + + deps += [ ":jni_headers" ] + } +} + +source_set("unit_tests") { + testonly = true + + visibility = [ "//components/feature_engagement:unit_tests" ] + + # IMPORTANT NOTE: When adding new tests, also remember to update the list of + # tests in //components/feature_engagement/components_unittests.filter + sources = [ + "availability_model_impl_unittest.cc", + "chrome_variations_configuration_unittest.cc", + "condition_validator_unittest.cc", + "configuration_unittest.cc", + "editable_configuration_unittest.cc", + "event_model_impl_unittest.cc", + "feature_config_condition_validator_unittest.cc", + "feature_config_event_storage_validator_unittest.cc", + "in_memory_event_store_unittest.cc", + "init_aware_event_model_unittest.cc", + "never_availability_model_unittest.cc", + "never_condition_validator_unittest.cc", + "never_event_storage_validator_unittest.cc", + "once_condition_validator_unittest.cc", + "persistent_availability_store_unittest.cc", + "persistent_event_store_unittest.cc", + "single_invalid_configuration_unittest.cc", + "system_time_provider_unittest.cc", + "tracker_impl_unittest.cc", + ] + + deps = [ + ":internal", + "//base/test:test_support", + "//components/feature_engagement/internal/test:test_support", + "//components/feature_engagement/public", + "//components/leveldb_proto:test_support", + "//testing/gmock", + "//testing/gtest", + ] +} + +if (is_android) { + android_library("internal_java") { + visibility = [ "//components/feature_engagement:feature_engagement_java" ] + + java_files = [ "android/java/src/org/chromium/components/feature_engagement/internal/TrackerImpl.java" ] + + deps = [ + "//base:base_java", + "//components/feature_engagement/public:public_java", + ] + } + + generate_jni("jni_headers") { + visibility = [ ":*" ] + sources = [ + "android/java/src/org/chromium/components/feature_engagement/internal/TrackerImpl.java", + ] + jni_package = "components/feature_engagement/internal" + } +} diff --git a/chromium/components/feature_engagement/internal/android/tracker_impl_android.cc b/chromium/components/feature_engagement/internal/android/tracker_impl_android.cc new file mode 100644 index 00000000000..0dc6aab10ec --- /dev/null +++ b/chromium/components/feature_engagement/internal/android/tracker_impl_android.cc @@ -0,0 +1,141 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/android/tracker_impl_android.h" + +#include <vector> + +#include "base/android/callback_android.h" +#include "base/android/jni_android.h" +#include "base/android/jni_string.h" +#include "base/android/scoped_java_ref.h" +#include "base/bind.h" +#include "base/feature_list.h" +#include "base/memory/ptr_util.h" +#include "components/feature_engagement/public/feature_list.h" +#include "components/feature_engagement/public/tracker.h" +#include "jni/TrackerImpl_jni.h" + +namespace feature_engagement { + +namespace { + +const char kTrackerImplAndroidKey[] = "tracker_impl_android"; + +// Create mapping from feature name to base::Feature. +TrackerImplAndroid::FeatureMap CreateMapFromNameToFeature( + FeatureVector features) { + TrackerImplAndroid::FeatureMap feature_map; + for (auto it = features.begin(); it != features.end(); ++it) { + feature_map[(*it)->name] = *it; + } + return feature_map; +} + +TrackerImplAndroid* FromTrackerImpl(Tracker* feature_engagement) { + TrackerImpl* impl = static_cast<TrackerImpl*>(feature_engagement); + TrackerImplAndroid* impl_android = static_cast<TrackerImplAndroid*>( + impl->GetUserData(kTrackerImplAndroidKey)); + if (!impl_android) { + impl_android = new TrackerImplAndroid(impl, GetAllFeatures()); + impl->SetUserData(kTrackerImplAndroidKey, base::WrapUnique(impl_android)); + } + return impl_android; +} + +} // namespace + +// static +TrackerImplAndroid* TrackerImplAndroid::FromJavaObject( + JNIEnv* env, + const base::android::JavaRef<jobject>& jobj) { + return reinterpret_cast<TrackerImplAndroid*>( + Java_TrackerImpl_getNativePtr(env, jobj)); +} + +// This function is declared in //components/feature_engagement/public/tracker.h +// and should be linked in to any binary using Tracker::GetJavaObject. +// static +base::android::ScopedJavaLocalRef<jobject> Tracker::GetJavaObject( + Tracker* feature_engagement) { + return FromTrackerImpl(feature_engagement)->GetJavaObject(); +} + +TrackerImplAndroid::TrackerImplAndroid(TrackerImpl* tracker_impl, + FeatureVector features) + : features_(CreateMapFromNameToFeature(features)), + tracker_impl_(tracker_impl) { + JNIEnv* env = base::android::AttachCurrentThread(); + + java_obj_.Reset( + env, + Java_TrackerImpl_create(env, reinterpret_cast<intptr_t>(this)).obj()); +} + +TrackerImplAndroid::~TrackerImplAndroid() { + Java_TrackerImpl_clearNativePtr(base::android::AttachCurrentThread(), + java_obj_); +} + +base::android::ScopedJavaLocalRef<jobject> TrackerImplAndroid::GetJavaObject() { + return base::android::ScopedJavaLocalRef<jobject>(java_obj_); +} + +void TrackerImplAndroid::NotifyEvent( + JNIEnv* env, + const base::android::JavaRef<jobject>& jobj, + const base::android::JavaParamRef<jstring>& jevent) { + std::string event = ConvertJavaStringToUTF8(env, jevent); + tracker_impl_->NotifyEvent(event); +} + +bool TrackerImplAndroid::ShouldTriggerHelpUI( + JNIEnv* env, + const base::android::JavaRef<jobject>& jobj, + const base::android::JavaParamRef<jstring>& jfeature) { + std::string feature = ConvertJavaStringToUTF8(env, jfeature); + DCHECK(features_.find(feature) != features_.end()); + + return tracker_impl_->ShouldTriggerHelpUI(*features_[feature]); +} + +jint TrackerImplAndroid::GetTriggerState( + JNIEnv* env, + const base::android::JavaRef<jobject>& jobj, + const base::android::JavaParamRef<jstring>& jfeature) { + std::string feature = ConvertJavaStringToUTF8(env, jfeature); + DCHECK(features_.find(feature) != features_.end()); + + return static_cast<int>(tracker_impl_->GetTriggerState(*features_[feature])); +} + +void TrackerImplAndroid::Dismissed( + JNIEnv* env, + const base::android::JavaRef<jobject>& jobj, + const base::android::JavaParamRef<jstring>& jfeature) { + std::string feature = ConvertJavaStringToUTF8(env, jfeature); + DCHECK(features_.find(feature) != features_.end()); + + tracker_impl_->Dismissed(*features_[feature]); +} + +bool TrackerImplAndroid::IsInitialized( + JNIEnv* env, + const base::android::JavaRef<jobject>& jobj) { + return tracker_impl_->IsInitialized(); +} + +void TrackerImplAndroid::AddOnInitializedCallback( + JNIEnv* env, + const base::android::JavaRef<jobject>& jobj, + const base::android::JavaParamRef<jobject>& j_callback_obj) { + // Disambiguate RunCallbackAndroid to get the reference to the bool version. + void (*runBoolCallback)(const base::android::JavaRef<jobject>&, bool) = + &base::android::RunCallbackAndroid; + tracker_impl_->AddOnInitializedCallback( + base::Bind(runBoolCallback, + base::android::ScopedJavaGlobalRef<jobject>(j_callback_obj))); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/android/tracker_impl_android.h b/chromium/components/feature_engagement/internal/android/tracker_impl_android.h new file mode 100644 index 00000000000..f429a1fba7c --- /dev/null +++ b/chromium/components/feature_engagement/internal/android/tracker_impl_android.h @@ -0,0 +1,80 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_ANDROID_TRACKER_IMPL_ANDROID_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_ANDROID_TRACKER_IMPL_ANDROID_H_ + +#include <string> +#include <unordered_map> + +#include "base/android/callback_android.h" +#include "base/android/jni_android.h" +#include "base/android/scoped_java_ref.h" +#include "base/macros.h" +#include "base/supports_user_data.h" +#include "components/feature_engagement/internal/tracker_impl.h" +#include "components/feature_engagement/public/feature_list.h" + +namespace base { +struct Feature; +} // namespace base + +namespace feature_engagement { + +// JNI bridge between TrackerImpl in Java and C++. See the +// public API of Tracker for documentation for all methods. +class TrackerImplAndroid : public base::SupportsUserData::Data { + public: + using FeatureMap = std::unordered_map<std::string, const base::Feature*>; + static TrackerImplAndroid* FromJavaObject( + JNIEnv* env, + const base::android::JavaRef<jobject>& jobj); + + TrackerImplAndroid(TrackerImpl* tracker_impl, FeatureVector features); + ~TrackerImplAndroid() override; + + base::android::ScopedJavaLocalRef<jobject> GetJavaObject(); + + TrackerImpl* tracker_impl() { return tracker_impl_; } + + // Tracker JNI bridge implementation. + virtual void NotifyEvent(JNIEnv* env, + const base::android::JavaRef<jobject>& jobj, + const base::android::JavaParamRef<jstring>& jevent); + virtual bool ShouldTriggerHelpUI( + JNIEnv* env, + const base::android::JavaRef<jobject>& jobj, + const base::android::JavaParamRef<jstring>& jfeature); + virtual jint GetTriggerState( + JNIEnv* env, + const base::android::JavaRef<jobject>& jobj, + const base::android::JavaParamRef<jstring>& jfeature); + virtual void Dismissed(JNIEnv* env, + const base::android::JavaRef<jobject>& jobj, + const base::android::JavaParamRef<jstring>& jfeature); + virtual bool IsInitialized(JNIEnv* env, + const base::android::JavaRef<jobject>& jobj); + virtual void AddOnInitializedCallback( + JNIEnv* env, + const base::android::JavaRef<jobject>& jobj, + const base::android::JavaParamRef<jobject>& j_callback_obj); + + private: + // A map from the feature name to the base::Feature, to ensure that the Java + // version of the API can use the string name. If base::Feature becomes a Java + // class as well, we should remove this mapping. + FeatureMap features_; + + // The TrackerImpl this is a JNI bridge for. + TrackerImpl* tracker_impl_; + + // The Java-side of this JNI bridge. + base::android::ScopedJavaGlobalRef<jobject> java_obj_; + + DISALLOW_COPY_AND_ASSIGN(TrackerImplAndroid); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_ANDROID_TRACKER_IMPL_ANDROID_H_ diff --git a/chromium/components/feature_engagement/internal/availability_model.h b/chromium/components/feature_engagement/internal/availability_model.h new file mode 100644 index 00000000000..27aea7e653a --- /dev/null +++ b/chromium/components/feature_engagement/internal/availability_model.h @@ -0,0 +1,54 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_AVAILABILITY_MODEL_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_AVAILABILITY_MODEL_H_ + +#include <stdint.h> + +#include "base/callback_forward.h" +#include "base/macros.h" +#include "base/optional.h" + +namespace base { +struct Feature; +} // namespace base + +namespace feature_engagement { + +// An AvailabilityModel tracks when each feature was made available to an +// end user. +class AvailabilityModel { + public: + // Invoked when the availability data has finished loading, and whether the + // load was a success. In the case of a failure, it is invalid to ever call + // GetAvailability(...). + using OnInitializedCallback = base::OnceCallback<void(bool success)>; + + virtual ~AvailabilityModel() = default; + + // Starts initialization of the AvailabilityModel. + virtual void Initialize(OnInitializedCallback callback, + uint32_t current_day) = 0; + + // Returns whether the model is ready, i.e. whether it has been successfully + // initialized. + virtual bool IsReady() const = 0; + + // Returns the day number since epoch (1970-01-01) in the local timezone for + // when the particular |feature| was made available. + // See TimeProvider::GetCurrentDay(). + virtual base::Optional<uint32_t> GetAvailability( + const base::Feature& feature) const = 0; + + protected: + AvailabilityModel() = default; + + private: + DISALLOW_COPY_AND_ASSIGN(AvailabilityModel); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_AVAILABILITY_MODEL_H_ diff --git a/chromium/components/feature_engagement/internal/availability_model_impl.cc b/chromium/components/feature_engagement/internal/availability_model_impl.cc new file mode 100644 index 00000000000..4844799f78b --- /dev/null +++ b/chromium/components/feature_engagement/internal/availability_model_impl.cc @@ -0,0 +1,62 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/availability_model_impl.h" + +#include <memory> +#include <utility> + +#include "base/bind.h" +#include "base/logging.h" +#include "base/memory/weak_ptr.h" +#include "components/feature_engagement/internal/persistent_availability_store.h" + +namespace feature_engagement { + +AvailabilityModelImpl::AvailabilityModelImpl( + StoreLoadCallback store_load_callback) + : ready_(false), + store_load_callback_(std::move(store_load_callback)), + weak_ptr_factory_(this) {} + +AvailabilityModelImpl::~AvailabilityModelImpl() = default; + +void AvailabilityModelImpl::Initialize(OnInitializedCallback callback, + uint32_t current_day) { + DCHECK(store_load_callback_); + std::move(store_load_callback_) + .Run(base::BindOnce(&AvailabilityModelImpl::OnStoreLoadComplete, + weak_ptr_factory_.GetWeakPtr(), std::move(callback)), + current_day); +} + +bool AvailabilityModelImpl::IsReady() const { + return ready_; +} + +base::Optional<uint32_t> AvailabilityModelImpl::GetAvailability( + const base::Feature& feature) const { + auto search = feature_availabilities_.find(feature.name); + if (search == feature_availabilities_.end()) + return base::nullopt; + + return search->second; +} + +void AvailabilityModelImpl::OnStoreLoadComplete( + OnInitializedCallback on_initialized_callback, + bool success, + std::unique_ptr<std::map<std::string, uint32_t>> feature_availabilities) { + if (!success) { + std::move(on_initialized_callback).Run(false); + return; + } + + feature_availabilities_ = std::move(*feature_availabilities); + + ready_ = true; + std::move(on_initialized_callback).Run(true); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/availability_model_impl.h b/chromium/components/feature_engagement/internal/availability_model_impl.h new file mode 100644 index 00000000000..7bd159695c2 --- /dev/null +++ b/chromium/components/feature_engagement/internal/availability_model_impl.h @@ -0,0 +1,69 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_AVAILABILITY_MODEL_IMPL_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_AVAILABILITY_MODEL_IMPL_H_ + +#include <stdint.h> + +#include <map> +#include <memory> +#include <string> + +#include "base/callback.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "components/feature_engagement/internal/availability_model.h" +#include "components/feature_engagement/internal/persistent_availability_store.h" + +namespace base { +struct Feature; +} // namespace base + +namespace feature_engagement { +// An AvailabilityModel which supports loading data from an +// PersistentAvailabilityStore. +class AvailabilityModelImpl : public AvailabilityModel { + public: + using StoreLoadCallback = + base::OnceCallback<void(PersistentAvailabilityStore::OnLoadedCallback, + uint32_t current_day)>; + + explicit AvailabilityModelImpl(StoreLoadCallback load_callback); + ~AvailabilityModelImpl() override; + + // AvailabilityModel implementation. + void Initialize(OnInitializedCallback callback, + uint32_t current_day) override; + bool IsReady() const override; + base::Optional<uint32_t> GetAvailability( + const base::Feature& feature) const override; + + private: + // This is invoked when the store has completed loading. + void OnStoreLoadComplete( + OnInitializedCallback on_initialized_callback, + bool success, + std::unique_ptr<std::map<std::string, uint32_t>> feature_availabilities); + + // Stores the day number since epoch (1970-01-01) in the local timezone for + // when the particular feature was made available. The key is the feature + // name. + std::map<std::string, uint32_t> feature_availabilities_; + + // Whether the model has successfully initialized. + bool ready_; + + // A callback for loading availability data from the store. This is reset + // as soon as it is invoked. + StoreLoadCallback store_load_callback_; + + base::WeakPtrFactory<AvailabilityModelImpl> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(AvailabilityModelImpl); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_AVAILABILITY_MODEL_IMPL_H_ diff --git a/chromium/components/feature_engagement/internal/availability_model_impl_unittest.cc b/chromium/components/feature_engagement/internal/availability_model_impl_unittest.cc new file mode 100644 index 00000000000..6c19feec6ed --- /dev/null +++ b/chromium/components/feature_engagement/internal/availability_model_impl_unittest.cc @@ -0,0 +1,134 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/availability_model_impl.h" + +#include <memory> +#include <utility> +#include <vector> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/macros.h" +#include "base/memory/ptr_util.h" +#include "base/optional.h" +#include "components/feature_engagement/internal/persistent_availability_store.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +namespace { + +const base::Feature kTestFeatureFoo{"test_foo", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureBar{"test_bar", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureQux{"test_qux", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureNop{"test_nop", + base::FEATURE_DISABLED_BY_DEFAULT}; + +class AvailabilityModelImplTest : public testing::Test { + public: + AvailabilityModelImplTest() { + initialized_callback_ = base::BindOnce( + &AvailabilityModelImplTest::OnInitialized, base::Unretained(this)); + } + + ~AvailabilityModelImplTest() override = default; + + // SetUpModel exists so that the filter can be changed for any test. + void SetUpModel( + bool success, + std::unique_ptr<std::map<std::string, uint32_t>> store_content) { + auto store_loader = base::BindOnce(&AvailabilityModelImplTest::StoreLoader, + base::Unretained(this), success, + std::move(store_content)); + availability_model_ = + base::MakeUnique<AvailabilityModelImpl>(std::move(store_loader)); + } + + void OnInitialized(bool success) { success_ = success; } + + void StoreLoader( + bool success, + std::unique_ptr<std::map<std::string, uint32_t>> store_content, + PersistentAvailabilityStore::OnLoadedCallback callback, + uint32_t current_day) { + current_day_ = current_day; + std::move(callback).Run(success, std::move(store_content)); + } + + protected: + std::unique_ptr<AvailabilityModelImpl> availability_model_; + + AvailabilityModel::OnInitializedCallback initialized_callback_; + base::Optional<bool> success_; + base::Optional<uint32_t> current_day_; + + private: + DISALLOW_COPY_AND_ASSIGN(AvailabilityModelImplTest); +}; + +} // namespace + +TEST_F(AvailabilityModelImplTest, InitializationSuccess) { + SetUpModel(true, base::MakeUnique<std::map<std::string, uint32_t>>()); + EXPECT_FALSE(availability_model_->IsReady()); + availability_model_->Initialize(std::move(initialized_callback_), 14u); + EXPECT_TRUE(availability_model_->IsReady()); + EXPECT_TRUE(success_.has_value()); + EXPECT_TRUE(success_.value()); + EXPECT_EQ(14u, current_day_); +} + +TEST_F(AvailabilityModelImplTest, InitializationFailed) { + SetUpModel(false, base::MakeUnique<std::map<std::string, uint32_t>>()); + EXPECT_FALSE(availability_model_->IsReady()); + availability_model_->Initialize(std::move(initialized_callback_), 14u); + EXPECT_FALSE(availability_model_->IsReady()); + EXPECT_TRUE(success_.has_value()); + EXPECT_FALSE(success_.value()); + EXPECT_EQ(14u, current_day_); +} + +TEST_F(AvailabilityModelImplTest, SuccessfullyLoadThreeFeatures) { + auto availabilities = base::MakeUnique<std::map<std::string, uint32_t>>(); + availabilities->insert(std::make_pair(kTestFeatureFoo.name, 100u)); + availabilities->insert(std::make_pair(kTestFeatureBar.name, 200u)); + availabilities->insert(std::make_pair(kTestFeatureNop.name, 300u)); + + SetUpModel(true, std::move(availabilities)); + availability_model_->Initialize(std::move(initialized_callback_), 14u); + EXPECT_TRUE(availability_model_->IsReady()); + + EXPECT_EQ(100u, availability_model_->GetAvailability(kTestFeatureFoo)); + EXPECT_EQ(200u, availability_model_->GetAvailability(kTestFeatureBar)); + EXPECT_EQ(300u, availability_model_->GetAvailability(kTestFeatureNop)); + EXPECT_EQ(base::nullopt, + availability_model_->GetAvailability(kTestFeatureQux)); +} + +TEST_F(AvailabilityModelImplTest, FailToLoadThreeFeatures) { + auto availabilities = base::MakeUnique<std::map<std::string, uint32_t>>(); + availabilities->insert(std::make_pair(kTestFeatureFoo.name, 100u)); + availabilities->insert(std::make_pair(kTestFeatureBar.name, 200u)); + availabilities->insert(std::make_pair(kTestFeatureNop.name, 300u)); + + SetUpModel(false, std::move(availabilities)); + availability_model_->Initialize(std::move(initialized_callback_), 14u); + EXPECT_FALSE(availability_model_->IsReady()); + + // Load failed, so all results should be ignored. + EXPECT_EQ(base::nullopt, + availability_model_->GetAvailability(kTestFeatureFoo)); + EXPECT_EQ(base::nullopt, + availability_model_->GetAvailability(kTestFeatureBar)); + EXPECT_EQ(base::nullopt, + availability_model_->GetAvailability(kTestFeatureNop)); + EXPECT_EQ(base::nullopt, + availability_model_->GetAvailability(kTestFeatureQux)); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/chrome_variations_configuration.cc b/chromium/components/feature_engagement/internal/chrome_variations_configuration.cc new file mode 100644 index 00000000000..c913e48ce3e --- /dev/null +++ b/chromium/components/feature_engagement/internal/chrome_variations_configuration.cc @@ -0,0 +1,334 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/chrome_variations_configuration.h" + +#include <map> +#include <memory> +#include <string> +#include <tuple> +#include <vector> + +#include "base/logging.h" +#include "base/memory/ptr_util.h" +#include "base/metrics/field_trial_params.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_piece.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "components/feature_engagement/internal/configuration.h" +#include "components/feature_engagement/internal/stats.h" +#include "components/feature_engagement/public/feature_list.h" + +namespace { + +const char kComparatorTypeAny[] = "any"; +const char kComparatorTypeLessThan[] = "<"; +const char kComparatorTypeGreaterThan[] = ">"; +const char kComparatorTypeLessThanOrEqual[] = "<="; +const char kComparatorTypeGreaterThanOrEqual[] = ">="; +const char kComparatorTypeEqual[] = "=="; +const char kComparatorTypeNotEqual[] = "!="; + +const char kEventConfigUsedKey[] = "event_used"; +const char kEventConfigTriggerKey[] = "event_trigger"; +const char kEventConfigKeyPrefix[] = "event_"; +const char kSessionRateKey[] = "session_rate"; +const char kAvailabilityKey[] = "availability"; +const char kIgnoredKeyPrefix[] = "x_"; + +const char kEventConfigDataNameKey[] = "name"; +const char kEventConfigDataComparatorKey[] = "comparator"; +const char kEventConfigDataWindowKey[] = "window"; +const char kEventConfigDataStorageKey[] = "storage"; + +} // namespace + +namespace feature_engagement { + +namespace { + +bool ParseComparatorSubstring(base::StringPiece definition, + Comparator* comparator, + ComparatorType type, + uint32_t type_len) { + base::StringPiece number_string = + base::TrimWhitespaceASCII(definition.substr(type_len), base::TRIM_ALL); + uint32_t value; + if (!base::StringToUint(number_string, &value)) + return false; + + comparator->type = type; + comparator->value = value; + return true; +} + +bool ParseComparator(base::StringPiece definition, Comparator* comparator) { + if (base::LowerCaseEqualsASCII(definition, kComparatorTypeAny)) { + comparator->type = ANY; + comparator->value = 0; + return true; + } + + if (base::StartsWith(definition, kComparatorTypeLessThanOrEqual, + base::CompareCase::INSENSITIVE_ASCII)) { + return ParseComparatorSubstring(definition, comparator, LESS_THAN_OR_EQUAL, + 2); + } + + if (base::StartsWith(definition, kComparatorTypeGreaterThanOrEqual, + base::CompareCase::INSENSITIVE_ASCII)) { + return ParseComparatorSubstring(definition, comparator, + GREATER_THAN_OR_EQUAL, 2); + } + + if (base::StartsWith(definition, kComparatorTypeEqual, + base::CompareCase::INSENSITIVE_ASCII)) { + return ParseComparatorSubstring(definition, comparator, EQUAL, 2); + } + + if (base::StartsWith(definition, kComparatorTypeNotEqual, + base::CompareCase::INSENSITIVE_ASCII)) { + return ParseComparatorSubstring(definition, comparator, NOT_EQUAL, 2); + } + + if (base::StartsWith(definition, kComparatorTypeLessThan, + base::CompareCase::INSENSITIVE_ASCII)) { + return ParseComparatorSubstring(definition, comparator, LESS_THAN, 1); + } + + if (base::StartsWith(definition, kComparatorTypeGreaterThan, + base::CompareCase::INSENSITIVE_ASCII)) { + return ParseComparatorSubstring(definition, comparator, GREATER_THAN, 1); + } + + return false; +} + +bool ParseEventConfig(base::StringPiece definition, EventConfig* event_config) { + // Support definitions with at least 4 tokens. + auto tokens = base::SplitStringPiece(definition, ";", base::TRIM_WHITESPACE, + base::SPLIT_WANT_ALL); + if (tokens.size() < 4) { + *event_config = EventConfig(); + return false; + } + + // Parse tokens in any order. + bool has_name = false; + bool has_comparator = false; + bool has_window = false; + bool has_storage = false; + for (const auto& token : tokens) { + auto pair = base::SplitStringPiece(token, ":", base::TRIM_WHITESPACE, + base::SPLIT_WANT_ALL); + if (pair.size() != 2) { + *event_config = EventConfig(); + return false; + } + + const base::StringPiece& key = pair[0]; + const base::StringPiece& value = pair[1]; + // TODO(nyquist): Ensure that key matches regex /^[a-zA-Z0-9-_]+$/. + + if (base::LowerCaseEqualsASCII(key, kEventConfigDataNameKey)) { + if (has_name) { + *event_config = EventConfig(); + return false; + } + has_name = true; + + event_config->name = value.as_string(); + } else if (base::LowerCaseEqualsASCII(key, kEventConfigDataComparatorKey)) { + if (has_comparator) { + *event_config = EventConfig(); + return false; + } + has_comparator = true; + + Comparator comparator; + if (!ParseComparator(value, &comparator)) { + *event_config = EventConfig(); + return false; + } + + event_config->comparator = comparator; + } else if (base::LowerCaseEqualsASCII(key, kEventConfigDataWindowKey)) { + if (has_window) { + *event_config = EventConfig(); + return false; + } + has_window = true; + + uint32_t parsed_value; + if (!base::StringToUint(value, &parsed_value)) { + *event_config = EventConfig(); + return false; + } + + event_config->window = parsed_value; + } else if (base::LowerCaseEqualsASCII(key, kEventConfigDataStorageKey)) { + if (has_storage) { + *event_config = EventConfig(); + return false; + } + has_storage = true; + + uint32_t parsed_value; + if (!base::StringToUint(value, &parsed_value)) { + *event_config = EventConfig(); + return false; + } + + event_config->storage = parsed_value; + } + } + + return has_name && has_comparator && has_window && has_storage; +} + +} // namespace + +ChromeVariationsConfiguration::ChromeVariationsConfiguration() = default; + +ChromeVariationsConfiguration::~ChromeVariationsConfiguration() = default; + +void ChromeVariationsConfiguration::ParseFeatureConfigs( + FeatureVector features) { + for (auto* feature : features) { + ParseFeatureConfig(feature); + } +} + +void ChromeVariationsConfiguration::ParseFeatureConfig( + const base::Feature* feature) { + DCHECK(feature); + DCHECK(configs_.find(feature->name) == configs_.end()); + + DVLOG(3) << "Parsing feature config for " << feature->name; + + // Initially all new configurations are considered invalid. + FeatureConfig& config = configs_[feature->name]; + config.valid = false; + uint32_t parse_errors = 0; + + std::map<std::string, std::string> params; + bool result = base::GetFieldTrialParamsByFeature(*feature, ¶ms); + if (!result) { + stats::RecordConfigParsingEvent( + stats::ConfigParsingEvent::FAILURE_NO_FIELD_TRIAL); + // Returns early. If no field trial, ConfigParsingEvent::FAILURE will not be + // recorded. + DVLOG(3) << "No field trial for " << feature->name; + return; + } + + for (const auto& it : params) { + const std::string& key = it.first; + if (key == kEventConfigUsedKey) { + EventConfig event_config; + if (!ParseEventConfig(params[key], &event_config)) { + ++parse_errors; + stats::RecordConfigParsingEvent( + stats::ConfigParsingEvent::FAILURE_USED_EVENT_PARSE); + continue; + } + config.used = event_config; + } else if (key == kEventConfigTriggerKey) { + EventConfig event_config; + if (!ParseEventConfig(params[key], &event_config)) { + stats::RecordConfigParsingEvent( + stats::ConfigParsingEvent::FAILURE_TRIGGER_EVENT_PARSE); + ++parse_errors; + continue; + } + config.trigger = event_config; + } else if (key == kSessionRateKey) { + Comparator comparator; + if (!ParseComparator(params[key], &comparator)) { + stats::RecordConfigParsingEvent( + stats::ConfigParsingEvent::FAILURE_SESSION_RATE_PARSE); + ++parse_errors; + continue; + } + config.session_rate = comparator; + } else if (key == kAvailabilityKey) { + Comparator comparator; + if (!ParseComparator(params[key], &comparator)) { + stats::RecordConfigParsingEvent( + stats::ConfigParsingEvent::FAILURE_AVAILABILITY_PARSE); + ++parse_errors; + continue; + } + config.availability = comparator; + } else if (base::StartsWith(key, kEventConfigKeyPrefix, + base::CompareCase::INSENSITIVE_ASCII)) { + EventConfig event_config; + if (!ParseEventConfig(params[key], &event_config)) { + stats::RecordConfigParsingEvent( + stats::ConfigParsingEvent::FAILURE_OTHER_EVENT_PARSE); + ++parse_errors; + continue; + } + config.event_configs.insert(event_config); + } else if (base::StartsWith(key, kIgnoredKeyPrefix, + base::CompareCase::INSENSITIVE_ASCII)) { + // Intentionally ignoring parameter using registered ignored prefix. + DVLOG(2) << "Ignoring unknown key when parsing config for feature " + << feature->name << ": " << key; + } else { + DVLOG(1) << "Unknown key found when parsing config for feature " + << feature->name << ": " << key; + stats::RecordConfigParsingEvent( + stats::ConfigParsingEvent::FAILURE_UNKNOWN_KEY); + } + } + + // The |used| and |trigger| members are required, so should not be the + // default values. + bool has_used_event = config.used != EventConfig(); + bool has_trigger_event = config.trigger != EventConfig(); + config.valid = has_used_event && has_trigger_event && parse_errors == 0; + + if (config.valid) { + stats::RecordConfigParsingEvent(stats::ConfigParsingEvent::SUCCESS); + DVLOG(2) << "Config for " << feature->name << " is valid."; + DVLOG(3) << "Config for " << feature->name << " = " << config; + } else { + stats::RecordConfigParsingEvent(stats::ConfigParsingEvent::FAILURE); + DVLOG(2) << "Config for " << feature->name << " is invalid."; + } + + // Notice parse errors for used and trigger events will also cause the + // following histograms being recorded. + if (!has_used_event) { + stats::RecordConfigParsingEvent( + stats::ConfigParsingEvent::FAILURE_USED_EVENT_MISSING); + } + if (!has_trigger_event) { + stats::RecordConfigParsingEvent( + stats::ConfigParsingEvent::FAILURE_TRIGGER_EVENT_MISSING); + } +} + +const FeatureConfig& ChromeVariationsConfiguration::GetFeatureConfig( + const base::Feature& feature) const { + auto it = configs_.find(feature.name); + DCHECK(it != configs_.end()); + return it->second; +} + +const FeatureConfig& ChromeVariationsConfiguration::GetFeatureConfigByName( + const std::string& feature_name) const { + auto it = configs_.find(feature_name); + DCHECK(it != configs_.end()); + return it->second; +} + +const Configuration::ConfigMap& +ChromeVariationsConfiguration::GetRegisteredFeatures() const { + return configs_; +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/chrome_variations_configuration.h b/chromium/components/feature_engagement/internal/chrome_variations_configuration.h new file mode 100644 index 00000000000..cfdf79b37ba --- /dev/null +++ b/chromium/components/feature_engagement/internal/chrome_variations_configuration.h @@ -0,0 +1,48 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_CHROME_VARIATIONS_CONFIGURATION_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_CHROME_VARIATIONS_CONFIGURATION_H_ + +#include "base/macros.h" +#include "components/feature_engagement/internal/configuration.h" +#include "components/feature_engagement/public/feature_list.h" + +namespace base { +struct Feature; +} // namespace base + +namespace feature_engagement { + +// A ChromeVariationsConfiguration provides a configuration that is parsed from +// Chrome variations feature params. It is required to call +// ParseFeatureConfigs(...) with all the features that should be parsed. +class ChromeVariationsConfiguration : public Configuration { + public: + ChromeVariationsConfiguration(); + ~ChromeVariationsConfiguration() override; + + // Configuration implementation. + const FeatureConfig& GetFeatureConfig( + const base::Feature& feature) const override; + const FeatureConfig& GetFeatureConfigByName( + const std::string& feature_name) const override; + const Configuration::ConfigMap& GetRegisteredFeatures() const override; + + // Parses the variations configuration for all of the given |features| and + // stores the result. It is only valid to call ParseFeatureConfig once. + void ParseFeatureConfigs(FeatureVector features); + + private: + void ParseFeatureConfig(const base::Feature* feature); + + // The current configurations. + ConfigMap configs_; + + DISALLOW_COPY_AND_ASSIGN(ChromeVariationsConfiguration); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_CHROME_VARIATIONS_CONFIGURATION_H_ diff --git a/chromium/components/feature_engagement/internal/chrome_variations_configuration_unittest.cc b/chromium/components/feature_engagement/internal/chrome_variations_configuration_unittest.cc new file mode 100644 index 00000000000..729837471b2 --- /dev/null +++ b/chromium/components/feature_engagement/internal/chrome_variations_configuration_unittest.cc @@ -0,0 +1,631 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/chrome_variations_configuration.h" + +#include <map> +#include <string> +#include <vector> + +#include "base/feature_list.h" +#include "base/metrics/field_trial.h" +#include "base/metrics/field_trial_param_associator.h" +#include "base/metrics/field_trial_params.h" +#include "base/test/histogram_tester.h" +#include "base/test/scoped_feature_list.h" +#include "components/feature_engagement/internal/configuration.h" +#include "components/feature_engagement/internal/stats.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +namespace { + +const base::Feature kTestFeatureFoo{"test_foo", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureBar{"test_bar", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureQux{"test_qux", + base::FEATURE_DISABLED_BY_DEFAULT}; + +const char kFooTrialName[] = "FooTrial"; +const char kBarTrialName[] = "BarTrial"; +const char kQuxTrialName[] = "QuxTrial"; +const char kGroupName[] = "Group1"; +const char kConfigParseEventName[] = "InProductHelp.Config.ParsingEvent"; + +class ChromeVariationsConfigurationTest : public ::testing::Test { + public: + ChromeVariationsConfigurationTest() : field_trials_(nullptr) { + base::FieldTrial* foo_trial = + base::FieldTrialList::CreateFieldTrial(kFooTrialName, kGroupName); + base::FieldTrial* bar_trial = + base::FieldTrialList::CreateFieldTrial(kBarTrialName, kGroupName); + base::FieldTrial* qux_trial = + base::FieldTrialList::CreateFieldTrial(kQuxTrialName, kGroupName); + trials_[kTestFeatureFoo.name] = foo_trial; + trials_[kTestFeatureBar.name] = bar_trial; + trials_[kTestFeatureQux.name] = qux_trial; + + std::unique_ptr<base::FeatureList> feature_list(new base::FeatureList); + feature_list->RegisterFieldTrialOverride( + kTestFeatureFoo.name, base::FeatureList::OVERRIDE_ENABLE_FEATURE, + foo_trial); + feature_list->RegisterFieldTrialOverride( + kTestFeatureBar.name, base::FeatureList::OVERRIDE_ENABLE_FEATURE, + bar_trial); + feature_list->RegisterFieldTrialOverride( + kTestFeatureQux.name, base::FeatureList::OVERRIDE_ENABLE_FEATURE, + qux_trial); + + scoped_feature_list.InitWithFeatureList(std::move(feature_list)); + EXPECT_EQ(foo_trial, base::FeatureList::GetFieldTrial(kTestFeatureFoo)); + EXPECT_EQ(bar_trial, base::FeatureList::GetFieldTrial(kTestFeatureBar)); + EXPECT_EQ(qux_trial, base::FeatureList::GetFieldTrial(kTestFeatureQux)); + } + + void TearDown() override { + // This is required to ensure each test can define its own params. + base::FieldTrialParamAssociator::GetInstance()->ClearAllParamsForTesting(); + } + + protected: + void SetFeatureParams(const base::Feature& feature, + std::map<std::string, std::string> params) { + ASSERT_TRUE( + base::FieldTrialParamAssociator::GetInstance() + ->AssociateFieldTrialParams(trials_[feature.name]->trial_name(), + kGroupName, params)); + + std::map<std::string, std::string> actualParams; + EXPECT_TRUE(base::GetFieldTrialParamsByFeature(feature, &actualParams)); + EXPECT_EQ(params, actualParams); + } + + void VerifyInvalid(const std::string& event_config) { + std::map<std::string, std::string> foo_params; + foo_params["event_used"] = "name:u;comparator:any;window:0;storage:1"; + foo_params["event_trigger"] = "name:et;comparator:any;window:0;storage:360"; + foo_params["event_0"] = event_config; + SetFeatureParams(kTestFeatureFoo, foo_params); + + std::vector<const base::Feature*> features = {&kTestFeatureFoo}; + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_FALSE(foo.valid); + } + + ChromeVariationsConfiguration configuration_; + + private: + base::FieldTrialList field_trials_; + std::map<std::string, base::FieldTrial*> trials_; + base::test::ScopedFeatureList scoped_feature_list; + + DISALLOW_COPY_AND_ASSIGN(ChromeVariationsConfigurationTest); +}; + +} // namespace + +TEST_F(ChromeVariationsConfigurationTest, + DisabledFeatureShouldHaveInvalidConfig) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({}, {kTestFeatureFoo}); + + FeatureVector features; + features.push_back(&kTestFeatureFoo); + base::HistogramTester histogram_tester; + + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo_config = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_FALSE(foo_config.valid); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>(stats::ConfigParsingEvent::FAILURE_NO_FIELD_TRIAL), 1); + histogram_tester.ExpectTotalCount(kConfigParseEventName, 1); +} + +TEST_F(ChromeVariationsConfigurationTest, ParseSingleFeature) { + std::map<std::string, std::string> foo_params; + foo_params["event_used"] = + "name:page_download_started;comparator:any;window:0;storage:360"; + foo_params["event_trigger"] = + "name:opened_chrome_home;comparator:any;window:0;storage:360"; + foo_params["event_1"] = + "name:user_has_seen_dino;comparator:>=1;window:120;storage:180"; + foo_params["event_2"] = + "name:user_opened_app_menu;comparator:<=0;window:120;storage:180"; + foo_params["event_3"] = + "name:user_opened_downloads_home;comparator:any;window:0;storage:360"; + SetFeatureParams(kTestFeatureFoo, foo_params); + + base::HistogramTester histogram_tester; + std::vector<const base::Feature*> features = {&kTestFeatureFoo}; + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_TRUE(foo.valid); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>(stats::ConfigParsingEvent::SUCCESS), 1); + histogram_tester.ExpectTotalCount(kConfigParseEventName, 1); + + FeatureConfig expected_foo; + expected_foo.valid = true; + expected_foo.used = + EventConfig("page_download_started", Comparator(ANY, 0), 0, 360); + expected_foo.trigger = + EventConfig("opened_chrome_home", Comparator(ANY, 0), 0, 360); + expected_foo.event_configs.insert(EventConfig( + "user_has_seen_dino", Comparator(GREATER_THAN_OR_EQUAL, 1), 120, 180)); + expected_foo.event_configs.insert(EventConfig( + "user_opened_app_menu", Comparator(LESS_THAN_OR_EQUAL, 0), 120, 180)); + expected_foo.event_configs.insert( + EventConfig("user_opened_downloads_home", Comparator(ANY, 0), 0, 360)); + EXPECT_EQ(expected_foo, foo); +} + +TEST_F(ChromeVariationsConfigurationTest, MissingUsedIsInvalid) { + std::map<std::string, std::string> foo_params; + foo_params["event_trigger"] = "name:et;comparator:any;window:0;storage:360"; + SetFeatureParams(kTestFeatureFoo, foo_params); + + base::HistogramTester histogram_tester; + std::vector<const base::Feature*> features = {&kTestFeatureFoo}; + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_FALSE(foo.valid); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>(stats::ConfigParsingEvent::FAILURE_USED_EVENT_MISSING), + 1); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>(stats::ConfigParsingEvent::FAILURE), 1); + histogram_tester.ExpectTotalCount(kConfigParseEventName, 2); +} + +TEST_F(ChromeVariationsConfigurationTest, MissingTriggerIsInvalid) { + std::map<std::string, std::string> foo_params; + foo_params["event_used"] = "name:eu;comparator:any;window:0;storage:360"; + SetFeatureParams(kTestFeatureFoo, foo_params); + + base::HistogramTester histogram_tester; + std::vector<const base::Feature*> features = {&kTestFeatureFoo}; + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_FALSE(foo.valid); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>( + stats::ConfigParsingEvent::FAILURE_TRIGGER_EVENT_MISSING), + 1); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>(stats::ConfigParsingEvent::FAILURE), 1); + histogram_tester.ExpectTotalCount(kConfigParseEventName, 2); +} + +TEST_F(ChromeVariationsConfigurationTest, OnlyTriggerAndUsedIsValid) { + std::map<std::string, std::string> foo_params; + foo_params["event_used"] = "name:eu;comparator:any;window:0;storage:360"; + foo_params["event_trigger"] = "name:et;comparator:any;window:0;storage:360"; + SetFeatureParams(kTestFeatureFoo, foo_params); + + base::HistogramTester histogram_tester; + std::vector<const base::Feature*> features = {&kTestFeatureFoo}; + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_TRUE(foo.valid); + + FeatureConfig expected_foo; + expected_foo.valid = true; + expected_foo.used = EventConfig("eu", Comparator(ANY, 0), 0, 360); + expected_foo.trigger = EventConfig("et", Comparator(ANY, 0), 0, 360); + EXPECT_EQ(expected_foo, foo); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>(stats::ConfigParsingEvent::SUCCESS), 1); + histogram_tester.ExpectTotalCount(kConfigParseEventName, 1); +} + +TEST_F(ChromeVariationsConfigurationTest, WhitespaceIsValid) { + std::map<std::string, std::string> foo_params; + foo_params["event_used"] = + " name : eu ; comparator : any ; window : 1 ; storage : 320 "; + foo_params["event_trigger"] = + " name:et;comparator : any ;window: 2;storage:330 "; + foo_params["event_0"] = "name:e0;comparator: <1 ;window:3\n;storage:340"; + foo_params["event_1"] = "name:e1;comparator: > 2 ;window:4;\rstorage:350"; + foo_params["event_2"] = "name:e2;comparator: <= 3 ;window:5;\tstorage:360"; + foo_params["event_3"] = "name:e3;comparator: <\n4 ;window:6;storage:370"; + foo_params["event_4"] = "name:e4;comparator: >\t5 ;window:7;storage:380"; + foo_params["event_5"] = "name:e5;comparator: <=\r6 ;window:8;storage:390"; + foo_params["event_6"] = "name:e6;comparator:\n<=7;window:9;storage:400"; + foo_params["event_7"] = "name:e7;comparator:<=8\n;window:10;storage:410"; + SetFeatureParams(kTestFeatureFoo, foo_params); + + std::vector<const base::Feature*> features = {&kTestFeatureFoo}; + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_TRUE(foo.valid); + + FeatureConfig expected_foo; + expected_foo.valid = true; + expected_foo.used = EventConfig("eu", Comparator(ANY, 0), 1, 320); + expected_foo.trigger = EventConfig("et", Comparator(ANY, 0), 2, 330); + expected_foo.event_configs.insert( + EventConfig("e0", Comparator(LESS_THAN, 1), 3, 340)); + expected_foo.event_configs.insert( + EventConfig("e1", Comparator(GREATER_THAN, 2), 4, 350)); + expected_foo.event_configs.insert( + EventConfig("e2", Comparator(LESS_THAN_OR_EQUAL, 3), 5, 360)); + expected_foo.event_configs.insert( + EventConfig("e3", Comparator(LESS_THAN, 4), 6, 370)); + expected_foo.event_configs.insert( + EventConfig("e4", Comparator(GREATER_THAN, 5), 7, 380)); + expected_foo.event_configs.insert( + EventConfig("e5", Comparator(LESS_THAN_OR_EQUAL, 6), 8, 390)); + expected_foo.event_configs.insert( + EventConfig("e6", Comparator(LESS_THAN_OR_EQUAL, 7), 9, 400)); + expected_foo.event_configs.insert( + EventConfig("e7", Comparator(LESS_THAN_OR_EQUAL, 8), 10, 410)); + EXPECT_EQ(expected_foo, foo); +} + +TEST_F(ChromeVariationsConfigurationTest, IgnoresInvalidConfigKeys) { + base::HistogramTester histogram_tester; + std::map<std::string, std::string> foo_params; + foo_params["event_used"] = "name:eu;comparator:any;window:0;storage:360"; + foo_params["event_trigger"] = "name:et;comparator:any;window:0;storage:360"; + foo_params["not_there_yet"] = "bogus value"; // Unrecognized. + foo_params["still_not_there"] = "another bogus value"; // Unrecognized. + foo_params["x_this_is_ignored"] = "this value is ignored"; // Ignored. + SetFeatureParams(kTestFeatureFoo, foo_params); + + std::vector<const base::Feature*> features = {&kTestFeatureFoo}; + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_TRUE(foo.valid); + + FeatureConfig expected_foo; + expected_foo.valid = true; + expected_foo.used = EventConfig("eu", Comparator(ANY, 0), 0, 360); + expected_foo.trigger = EventConfig("et", Comparator(ANY, 0), 0, 360); + EXPECT_EQ(expected_foo, foo); + + // Exactly 2 keys should be unrecognized and not ignored. + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>(stats::ConfigParsingEvent::FAILURE_UNKNOWN_KEY), 2); +} + +TEST_F(ChromeVariationsConfigurationTest, IgnoresInvalidEventConfigTokens) { + std::map<std::string, std::string> foo_params; + foo_params["event_used"] = + "name:eu;comparator:any;window:0;storage:360;somethingelse:1"; + foo_params["event_trigger"] = + "yesway:0;noway:1;name:et;comparator:any;window:0;storage:360"; + SetFeatureParams(kTestFeatureFoo, foo_params); + + std::vector<const base::Feature*> features = {&kTestFeatureFoo}; + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_TRUE(foo.valid); + + FeatureConfig expected_foo; + expected_foo.valid = true; + expected_foo.used = EventConfig("eu", Comparator(ANY, 0), 0, 360); + expected_foo.trigger = EventConfig("et", Comparator(ANY, 0), 0, 360); + EXPECT_EQ(expected_foo, foo); +} + +TEST_F(ChromeVariationsConfigurationTest, + MissingAllEventConfigParamsIsInvalid) { + VerifyInvalid("a:1;b:2;c:3;d:4"); +} + +TEST_F(ChromeVariationsConfigurationTest, + MissingEventEventConfigParamIsInvalid) { + VerifyInvalid("foobar:eu;comparator:any;window:1;storage:360"); +} + +TEST_F(ChromeVariationsConfigurationTest, + MissingComparatorEventConfigParamIsInvalid) { + VerifyInvalid("name:eu;foobar:any;window:1;storage:360"); +} + +TEST_F(ChromeVariationsConfigurationTest, + MissingWindowEventConfigParamIsInvalid) { + VerifyInvalid("name:eu;comparator:any;foobar:1;storage:360"); +} + +TEST_F(ChromeVariationsConfigurationTest, + MissingStorageEventConfigParamIsInvalid) { + VerifyInvalid("name:eu;comparator:any;window:1;foobar:360"); +} + +TEST_F(ChromeVariationsConfigurationTest, + EventSpecifiedMultipleTimesIsInvalid) { + VerifyInvalid("name:eu;name:eu;comparator:any;window:1;storage:360"); +} + +TEST_F(ChromeVariationsConfigurationTest, + ComparatorSpecifiedMultipleTimesIsInvalid) { + VerifyInvalid("name:eu;comparator:any;comparator:any;window:1;storage:360"); +} + +TEST_F(ChromeVariationsConfigurationTest, + WindowSpecifiedMultipleTimesIsInvalid) { + VerifyInvalid("name:eu;comparator:any;window:1;window:2;storage:360"); +} + +TEST_F(ChromeVariationsConfigurationTest, + StorageSpecifiedMultipleTimesIsInvalid) { + VerifyInvalid("name:eu;comparator:any;window:1;storage:360;storage:10"); +} + +TEST_F(ChromeVariationsConfigurationTest, + InvalidSessionRateCausesInvalidConfig) { + std::map<std::string, std::string> foo_params; + foo_params["event_used"] = "name:eu;comparator:any;window:1;storage:360"; + foo_params["event_trigger"] = "name:et;comparator:any;window:0;storage:360"; + foo_params["session_rate"] = "bogus value"; + SetFeatureParams(kTestFeatureFoo, foo_params); + + base::HistogramTester histogram_tester; + std::vector<const base::Feature*> features = {&kTestFeatureFoo}; + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_FALSE(foo.valid); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>(stats::ConfigParsingEvent::FAILURE_SESSION_RATE_PARSE), + 1); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>(stats::ConfigParsingEvent::FAILURE), 1); + histogram_tester.ExpectTotalCount(kConfigParseEventName, 2); +} + +TEST_F(ChromeVariationsConfigurationTest, + InvalidAvailabilityCausesInvalidConfig) { + std::map<std::string, std::string> foo_params; + foo_params["event_used"] = "name:eu;comparator:any;window:0;storage:360"; + foo_params["event_trigger"] = "name:et;comparator:any;window:0;storage:360"; + foo_params["availability"] = "bogus value"; + SetFeatureParams(kTestFeatureFoo, foo_params); + + base::HistogramTester histogram_tester; + std::vector<const base::Feature*> features = {&kTestFeatureFoo}; + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_FALSE(foo.valid); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>(stats::ConfigParsingEvent::FAILURE_AVAILABILITY_PARSE), + 1); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>(stats::ConfigParsingEvent::FAILURE), 1); + histogram_tester.ExpectTotalCount(kConfigParseEventName, 2); +} + +TEST_F(ChromeVariationsConfigurationTest, InvalidUsedCausesInvalidConfig) { + std::map<std::string, std::string> foo_params; + foo_params["event_used"] = "bogus value"; + foo_params["event_trigger"] = "name:et;comparator:any;window:0;storage:360"; + SetFeatureParams(kTestFeatureFoo, foo_params); + + base::HistogramTester histogram_tester; + std::vector<const base::Feature*> features = {&kTestFeatureFoo}; + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_FALSE(foo.valid); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>(stats::ConfigParsingEvent::FAILURE_USED_EVENT_PARSE), 1); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>(stats::ConfigParsingEvent::FAILURE_USED_EVENT_MISSING), + 1); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>(stats::ConfigParsingEvent::FAILURE), 1); + histogram_tester.ExpectTotalCount(kConfigParseEventName, 3); +} + +TEST_F(ChromeVariationsConfigurationTest, InvalidTriggerCausesInvalidConfig) { + std::map<std::string, std::string> foo_params; + foo_params["event_used"] = "name:eu;comparator:any;window:0;storage:360"; + foo_params["event_trigger"] = "bogus value"; + SetFeatureParams(kTestFeatureFoo, foo_params); + + base::HistogramTester histogram_tester; + std::vector<const base::Feature*> features = {&kTestFeatureFoo}; + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_FALSE(foo.valid); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>(stats::ConfigParsingEvent::FAILURE_TRIGGER_EVENT_PARSE), + 1); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>( + stats::ConfigParsingEvent::FAILURE_TRIGGER_EVENT_MISSING), + 1); + histogram_tester.ExpectBucketCount( + kConfigParseEventName, + static_cast<int>(stats::ConfigParsingEvent::FAILURE), 1); + histogram_tester.ExpectTotalCount(kConfigParseEventName, 3); +} + +TEST_F(ChromeVariationsConfigurationTest, + InvalidEventConfigCausesInvalidConfig) { + std::map<std::string, std::string> foo_params; + foo_params["event_used"] = "name:eu;comparator:any;window:0;storage:360"; + foo_params["event_trigger"] = "name:et;comparator:any;window:0;storage:360"; + foo_params["event_used_0"] = "bogus value"; + SetFeatureParams(kTestFeatureFoo, foo_params); + + std::vector<const base::Feature*> features = {&kTestFeatureFoo}; + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_FALSE(foo.valid); +} + +TEST_F(ChromeVariationsConfigurationTest, AllComparatorTypesWork) { + std::map<std::string, std::string> foo_params; + foo_params["event_used"] = "name:e0;comparator:any;window:20;storage:30"; + foo_params["event_trigger"] = "name:e1;comparator:<1;window:21;storage:31"; + foo_params["event_2"] = "name:e2;comparator:>2;window:22;storage:32"; + foo_params["event_3"] = "name:e3;comparator:<=3;window:23;storage:33"; + foo_params["event_4"] = "name:e4;comparator:>=4;window:24;storage:34"; + foo_params["event_5"] = "name:e5;comparator:==5;window:25;storage:35"; + foo_params["event_6"] = "name:e6;comparator:!=6;window:26;storage:36"; + foo_params["session_rate"] = "!=6"; + foo_params["availability"] = ">=1"; + SetFeatureParams(kTestFeatureFoo, foo_params); + + std::vector<const base::Feature*> features = {&kTestFeatureFoo}; + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_TRUE(foo.valid); + + FeatureConfig expected_foo; + expected_foo.valid = true; + expected_foo.used = EventConfig("e0", Comparator(ANY, 0), 20, 30); + expected_foo.trigger = EventConfig("e1", Comparator(LESS_THAN, 1), 21, 31); + expected_foo.event_configs.insert( + EventConfig("e2", Comparator(GREATER_THAN, 2), 22, 32)); + expected_foo.event_configs.insert( + EventConfig("e3", Comparator(LESS_THAN_OR_EQUAL, 3), 23, 33)); + expected_foo.event_configs.insert( + EventConfig("e4", Comparator(GREATER_THAN_OR_EQUAL, 4), 24, 34)); + expected_foo.event_configs.insert( + EventConfig("e5", Comparator(EQUAL, 5), 25, 35)); + expected_foo.event_configs.insert( + EventConfig("e6", Comparator(NOT_EQUAL, 6), 26, 36)); + expected_foo.session_rate = Comparator(NOT_EQUAL, 6); + expected_foo.availability = Comparator(GREATER_THAN_OR_EQUAL, 1); + EXPECT_EQ(expected_foo, foo); +} + +TEST_F(ChromeVariationsConfigurationTest, MultipleEventsWithSameName) { + std::map<std::string, std::string> foo_params; + foo_params["event_used"] = "name:foo;comparator:any;window:20;storage:30"; + foo_params["event_trigger"] = "name:foo;comparator:<1;window:21;storage:31"; + foo_params["event_2"] = "name:foo;comparator:>2;window:22;storage:32"; + foo_params["event_3"] = "name:foo;comparator:<=3;window:23;storage:33"; + foo_params["session_rate"] = "any"; + foo_params["availability"] = ">1"; + SetFeatureParams(kTestFeatureFoo, foo_params); + + std::vector<const base::Feature*> features = {&kTestFeatureFoo}; + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_TRUE(foo.valid); + + FeatureConfig expected_foo; + expected_foo.valid = true; + expected_foo.used = EventConfig("foo", Comparator(ANY, 0), 20, 30); + expected_foo.trigger = EventConfig("foo", Comparator(LESS_THAN, 1), 21, 31); + expected_foo.event_configs.insert( + EventConfig("foo", Comparator(GREATER_THAN, 2), 22, 32)); + expected_foo.event_configs.insert( + EventConfig("foo", Comparator(LESS_THAN_OR_EQUAL, 3), 23, 33)); + expected_foo.session_rate = Comparator(ANY, 0); + expected_foo.availability = Comparator(GREATER_THAN, 1); + EXPECT_EQ(expected_foo, foo); +} + +TEST_F(ChromeVariationsConfigurationTest, ParseMultipleFeatures) { + std::map<std::string, std::string> foo_params; + foo_params["event_used"] = + "name:foo_used;comparator:any;window:20;storage:30"; + foo_params["event_trigger"] = + "name:foo_trigger;comparator:<1;window:21;storage:31"; + foo_params["session_rate"] = "==10"; + foo_params["availability"] = "<0"; + SetFeatureParams(kTestFeatureFoo, foo_params); + + std::map<std::string, std::string> bar_params; + bar_params["event_used"] = "name:bar_used;comparator:ANY;window:0;storage:0"; + SetFeatureParams(kTestFeatureBar, bar_params); + + std::map<std::string, std::string> qux_params; + qux_params["event_used"] = + "name:qux_used;comparator:>=2;window:99;storage:42"; + qux_params["event_trigger"] = + "name:qux_trigger;comparator:ANY;window:103;storage:104"; + qux_params["event_0"] = "name:q0;comparator:<10;window:20;storage:30"; + qux_params["event_1"] = "name:q1;comparator:>11;window:21;storage:31"; + qux_params["event_2"] = "name:q2;comparator:<=12;window:22;storage:32"; + qux_params["event_3"] = "name:q3;comparator:>=13;window:23;storage:33"; + qux_params["event_4"] = "name:q4;comparator:==14;window:24;storage:34"; + qux_params["event_5"] = "name:q5;comparator:!=15;window:25;storage:35"; + qux_params["session_rate"] = "!=13"; + qux_params["availability"] = "==0"; + SetFeatureParams(kTestFeatureQux, qux_params); + + std::vector<const base::Feature*> features = { + &kTestFeatureFoo, &kTestFeatureBar, &kTestFeatureQux}; + configuration_.ParseFeatureConfigs(features); + + FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_TRUE(foo.valid); + FeatureConfig expected_foo; + expected_foo.valid = true; + expected_foo.used = EventConfig("foo_used", Comparator(ANY, 0), 20, 30); + expected_foo.trigger = + EventConfig("foo_trigger", Comparator(LESS_THAN, 1), 21, 31); + expected_foo.session_rate = Comparator(EQUAL, 10); + expected_foo.availability = Comparator(LESS_THAN, 0); + EXPECT_EQ(expected_foo, foo); + + FeatureConfig bar = configuration_.GetFeatureConfig(kTestFeatureBar); + EXPECT_FALSE(bar.valid); + + FeatureConfig qux = configuration_.GetFeatureConfig(kTestFeatureQux); + EXPECT_TRUE(qux.valid); + FeatureConfig expected_qux; + expected_qux.valid = true; + expected_qux.used = + EventConfig("qux_used", Comparator(GREATER_THAN_OR_EQUAL, 2), 99, 42); + expected_qux.trigger = + EventConfig("qux_trigger", Comparator(ANY, 0), 103, 104); + expected_qux.event_configs.insert( + EventConfig("q0", Comparator(LESS_THAN, 10), 20, 30)); + expected_qux.event_configs.insert( + EventConfig("q1", Comparator(GREATER_THAN, 11), 21, 31)); + expected_qux.event_configs.insert( + EventConfig("q2", Comparator(LESS_THAN_OR_EQUAL, 12), 22, 32)); + expected_qux.event_configs.insert( + EventConfig("q3", Comparator(GREATER_THAN_OR_EQUAL, 13), 23, 33)); + expected_qux.event_configs.insert( + EventConfig("q4", Comparator(EQUAL, 14), 24, 34)); + expected_qux.event_configs.insert( + EventConfig("q5", Comparator(NOT_EQUAL, 15), 25, 35)); + expected_qux.session_rate = Comparator(NOT_EQUAL, 13); + expected_qux.availability = Comparator(EQUAL, 0); + EXPECT_EQ(expected_qux, qux); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/condition_validator.cc b/chromium/components/feature_engagement/internal/condition_validator.cc new file mode 100644 index 00000000000..61a57e20ef2 --- /dev/null +++ b/chromium/components/feature_engagement/internal/condition_validator.cc @@ -0,0 +1,57 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/condition_validator.h" + +#include <ostream> + +namespace feature_engagement { + +ConditionValidator::Result::Result(bool initial_values) + : event_model_ready_ok(initial_values), + currently_showing_ok(initial_values), + feature_enabled_ok(initial_values), + config_ok(initial_values), + used_ok(initial_values), + trigger_ok(initial_values), + preconditions_ok(initial_values), + session_rate_ok(initial_values), + availability_model_ready_ok(initial_values), + availability_ok(initial_values) {} + +ConditionValidator::Result::Result(const Result& other) { + event_model_ready_ok = other.event_model_ready_ok; + currently_showing_ok = other.currently_showing_ok; + feature_enabled_ok = other.feature_enabled_ok; + config_ok = other.config_ok; + used_ok = other.used_ok; + trigger_ok = other.trigger_ok; + preconditions_ok = other.preconditions_ok; + session_rate_ok = other.session_rate_ok; + availability_model_ready_ok = other.availability_model_ready_ok; + availability_ok = other.availability_ok; +} + +bool ConditionValidator::Result::NoErrors() const { + return event_model_ready_ok && currently_showing_ok && feature_enabled_ok && + config_ok && used_ok && trigger_ok && preconditions_ok && + session_rate_ok && availability_model_ready_ok && availability_ok; +} + +std::ostream& operator<<(std::ostream& os, + const ConditionValidator::Result& result) { + return os << "{ event_model_ready_ok=" << result.event_model_ready_ok + << ", currently_showing_ok=" << result.currently_showing_ok + << ", feature_enabled_ok=" << result.feature_enabled_ok + << ", config_ok=" << result.config_ok + << ", used_ok=" << result.used_ok + << ", trigger_ok=" << result.trigger_ok + << ", preconditions_ok=" << result.preconditions_ok + << ", session_rate_ok=" << result.session_rate_ok + << ", availability_model_ready_ok=" + << result.availability_model_ready_ok + << ", availability_ok=" << result.availability_ok << " }"; +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/condition_validator.h b/chromium/components/feature_engagement/internal/condition_validator.h new file mode 100644 index 00000000000..809f4579c6e --- /dev/null +++ b/chromium/components/feature_engagement/internal/condition_validator.h @@ -0,0 +1,99 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_CONDITION_VALIDATOR_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_CONDITION_VALIDATOR_H_ + +#include <stdint.h> + +#include <ostream> +#include <string> + +#include "base/macros.h" +#include "components/feature_engagement/public/feature_list.h" + +namespace base { +struct Feature; +} // namespace base + +namespace feature_engagement { +struct FeatureConfig; +class AvailabilityModel; +class EventModel; + +// A ConditionValidator checks the requred conditions for a given feature, +// and checks if all conditions are met. +class ConditionValidator { + public: + // The Result struct is used to categorize everything that could have the + // wrong state. By returning an instance of this where every value is true + // from MeetsConditions(...), it can be assumed that in-product help will + // be displayed. + struct Result { + explicit Result(bool initial_values); + Result(const Result& other); + + // Whether the event model was ready. + bool event_model_ready_ok; + + // Whether no other in-product helps were shown at the time. + bool currently_showing_ok; + + // Whether the feature is enabled. + bool feature_enabled_ok; + + // Whether the feature configuration was valid. + bool config_ok; + + // Whether the used precondition was met. + bool used_ok; + + // Whether the trigger precondition was met. + bool trigger_ok; + + // Whether the other preconditions were met. + bool preconditions_ok; + + // Whether the session rate precondition was met. + bool session_rate_ok; + + // Whether the availability model was ready. + bool availability_model_ready_ok; + + // Whether the availability precondition was met. + bool availability_ok; + + // Returns true if this result object has no errors, i.e. no values that + // are false. + bool NoErrors() const; + }; + + virtual ~ConditionValidator() = default; + + // Returns a Result object that describes whether each condition has been met. + virtual Result MeetsConditions(const base::Feature& feature, + const FeatureConfig& config, + const EventModel& event_model, + const AvailabilityModel& availability_model, + uint32_t current_day) const = 0; + + // Must be called to notify that the |feature| is currently showing. + virtual void NotifyIsShowing(const base::Feature& feature) = 0; + + // Must be called to notify that the |feature| is no longer showing. + virtual void NotifyDismissed(const base::Feature& feature) = 0; + + protected: + ConditionValidator() = default; + + private: + DISALLOW_COPY_AND_ASSIGN(ConditionValidator); +}; + +std::ostream& operator<<(std::ostream& os, + const ConditionValidator::Result& result); + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_CONDITION_VALIDATOR_H_ diff --git a/chromium/components/feature_engagement/internal/condition_validator_unittest.cc b/chromium/components/feature_engagement/internal/condition_validator_unittest.cc new file mode 100644 index 00000000000..381632fe15e --- /dev/null +++ b/chromium/components/feature_engagement/internal/condition_validator_unittest.cc @@ -0,0 +1,86 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/condition_validator.h" + +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +TEST(ConditionValidatorResultTest, TestAllOK) { + EXPECT_TRUE(ConditionValidator::Result(true).NoErrors()); +} + +TEST(ConditionValidatorResultTest, TestAllErrors) { + EXPECT_FALSE(ConditionValidator::Result(false).NoErrors()); +} + +TEST(ConditionValidatorResultTest, TestModelNotReady) { + ConditionValidator::Result result(true); + result.event_model_ready_ok = false; + EXPECT_FALSE(result.NoErrors()); +} + +TEST(ConditionValidatorResultTest, TestCurrentlyShowing) { + ConditionValidator::Result result(true); + result.currently_showing_ok = false; + EXPECT_FALSE(result.NoErrors()); +} + +TEST(ConditionValidatorResultTest, TestFeatureEnabled) { + ConditionValidator::Result result(true); + result.feature_enabled_ok = false; + EXPECT_FALSE(result.NoErrors()); +} + +TEST(ConditionValidatorResultTest, TestInvalidConfig) { + ConditionValidator::Result result(true); + result.config_ok = false; + EXPECT_FALSE(result.NoErrors()); +} + +TEST(ConditionValidatorResultTest, TestUsedFailed) { + ConditionValidator::Result result(true); + result.used_ok = false; + EXPECT_FALSE(result.NoErrors()); +} + +TEST(ConditionValidatorResultTest, TestTriggerFailed) { + ConditionValidator::Result result(true); + result.trigger_ok = false; + EXPECT_FALSE(result.NoErrors()); +} + +TEST(ConditionValidatorResultTest, TestPreconditionsFailed) { + ConditionValidator::Result result(true); + result.preconditions_ok = false; + EXPECT_FALSE(result.NoErrors()); +} + +TEST(ConditionValidatorResultTest, TestSessionRateFailed) { + ConditionValidator::Result result(true); + result.session_rate_ok = false; + EXPECT_FALSE(result.NoErrors()); +} + +TEST(ConditionValidatorResultTest, TestAvailabilityModelNotReady) { + ConditionValidator::Result result(true); + result.availability_model_ready_ok = false; + EXPECT_FALSE(result.NoErrors()); +} + +TEST(ConditionValidatorResultTest, TestAvailabilityFailed) { + ConditionValidator::Result result(true); + result.availability_ok = false; + EXPECT_FALSE(result.NoErrors()); +} + +TEST(ConditionValidatorResultTest, TestMultipleErrors) { + ConditionValidator::Result result(true); + result.preconditions_ok = false; + result.session_rate_ok = false; + EXPECT_FALSE(result.NoErrors()); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/configuration.cc b/chromium/components/feature_engagement/internal/configuration.cc new file mode 100644 index 00000000000..048eb63d79d --- /dev/null +++ b/chromium/components/feature_engagement/internal/configuration.cc @@ -0,0 +1,135 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/configuration.h" + +#include <string> + +#include "base/logging.h" + +namespace feature_engagement { + +Comparator::Comparator() : type(ANY), value(0) {} + +Comparator::Comparator(ComparatorType type, uint32_t value) + : type(type), value(value) {} + +Comparator::~Comparator() = default; + +bool Comparator::MeetsCriteria(uint32_t v) const { + switch (type) { + case ANY: + return true; + case LESS_THAN: + return v < value; + case GREATER_THAN: + return v > value; + case LESS_THAN_OR_EQUAL: + return v <= value; + case GREATER_THAN_OR_EQUAL: + return v >= value; + case EQUAL: + return v == value; + case NOT_EQUAL: + return v != value; + default: + // All cases should be covered. + NOTREACHED(); + return false; + } +} + +std::ostream& operator<<(std::ostream& os, const Comparator& comparator) { + switch (comparator.type) { + case ANY: + return os << "ANY"; + case LESS_THAN: + return os << "<" << comparator.value; + case GREATER_THAN: + return os << ">" << comparator.value; + case LESS_THAN_OR_EQUAL: + return os << "<=" << comparator.value; + case GREATER_THAN_OR_EQUAL: + return os << ">=" << comparator.value; + case EQUAL: + return os << "==" << comparator.value; + case NOT_EQUAL: + return os << "!=" << comparator.value; + default: + // All cases should be covered. + NOTREACHED(); + return os; + } +} + +EventConfig::EventConfig() : window(0), storage(0) {} + +EventConfig::EventConfig(const std::string& name, + Comparator comparator, + uint32_t window, + uint32_t storage) + : name(name), comparator(comparator), window(window), storage(storage) {} + +EventConfig::~EventConfig() = default; + +std::ostream& operator<<(std::ostream& os, const EventConfig& event_config) { + return os << "{ name: " << event_config.name + << ", comparator: " << event_config.comparator + << ", window: " << event_config.window + << ", storage: " << event_config.storage << " }"; +} + +FeatureConfig::FeatureConfig() : valid(false) {} + +FeatureConfig::FeatureConfig(const FeatureConfig& other) = default; + +FeatureConfig::~FeatureConfig() = default; + +bool operator==(const Comparator& lhs, const Comparator& rhs) { + return std::tie(lhs.type, lhs.value) == std::tie(rhs.type, rhs.value); +} + +bool operator<(const Comparator& lhs, const Comparator& rhs) { + return std::tie(lhs.type, lhs.value) < std::tie(rhs.type, rhs.value); +} + +bool operator==(const EventConfig& lhs, const EventConfig& rhs) { + return std::tie(lhs.name, lhs.comparator, lhs.window, lhs.storage) == + std::tie(rhs.name, rhs.comparator, rhs.window, rhs.storage); +} + +bool operator!=(const EventConfig& lhs, const EventConfig& rhs) { + return !(lhs == rhs); +} + +bool operator<(const EventConfig& lhs, const EventConfig& rhs) { + return std::tie(lhs.name, lhs.comparator, lhs.window, lhs.storage) < + std::tie(rhs.name, rhs.comparator, rhs.window, rhs.storage); +} + +bool operator==(const FeatureConfig& lhs, const FeatureConfig& rhs) { + return std::tie(lhs.valid, lhs.used, lhs.trigger, lhs.event_configs, + lhs.session_rate, lhs.availability) == + std::tie(rhs.valid, rhs.used, rhs.trigger, rhs.event_configs, + rhs.session_rate, rhs.availability); +} + +std::ostream& operator<<(std::ostream& os, + const FeatureConfig& feature_config) { + os << "{ valid: " << feature_config.valid << ", used: " << feature_config.used + << ", trigger: " << feature_config.trigger << ", event_configs: ["; + bool first = true; + for (const auto& event_config : feature_config.event_configs) { + if (first) { + first = false; + os << event_config; + } else { + os << ", " << event_config; + } + } + return os << "], session_rate: " << feature_config.session_rate + << ", availability: " << feature_config.availability << " }"; +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/configuration.h b/chromium/components/feature_engagement/internal/configuration.h new file mode 100644 index 00000000000..891a62e2362 --- /dev/null +++ b/chromium/components/feature_engagement/internal/configuration.h @@ -0,0 +1,149 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_CONFIGURATION_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_CONFIGURATION_H_ + +#include <map> +#include <ostream> +#include <set> +#include <string> +#include <vector> + +#include "base/macros.h" + +namespace base { +struct Feature; +} + +namespace feature_engagement { + +// A ComparatorType describes the relationship between two numbers. +enum ComparatorType { + ANY = 0, // Will always yield true. + LESS_THAN = 1, + GREATER_THAN = 2, + LESS_THAN_OR_EQUAL = 3, + GREATER_THAN_OR_EQUAL = 4, + EQUAL = 5, + NOT_EQUAL = 6, +}; + +// A Comparator provides a way of comparing a uint32_t another uint32_t and +// verifying their relationship. +struct Comparator { + public: + Comparator(); + Comparator(ComparatorType type, uint32_t value); + ~Comparator(); + + // Returns true if the |v| meets the this criteria based on the current + // |type| and |value|. + bool MeetsCriteria(uint32_t v) const; + + ComparatorType type; + uint32_t value; +}; + +bool operator==(const Comparator& lhs, const Comparator& rhs); +bool operator<(const Comparator& lhs, const Comparator& rhs); +std::ostream& operator<<(std::ostream& os, const Comparator& comparator); + +// A EventConfig contains all the information about how many times +// a particular event should or should not have triggered, for which window +// to search in and for how long to store it. +struct EventConfig { + public: + EventConfig(); + EventConfig(const std::string& name, + Comparator comparator, + uint32_t window, + uint32_t storage); + ~EventConfig(); + + // The identifier of the event. + std::string name; + + // The number of events it is required to find within the search window. + Comparator comparator; + + // Search for this event within this window. + uint32_t window; + + // Store client side data related to events for this minimum this long. + uint32_t storage; +}; + +bool operator==(const EventConfig& lhs, const EventConfig& rhs); +bool operator!=(const EventConfig& lhs, const EventConfig& rhs); +bool operator<(const EventConfig& lhs, const EventConfig& rhs); +std::ostream& operator<<(std::ostream& os, const EventConfig& event_config); + +// A FeatureConfig contains all the configuration for a given feature. +struct FeatureConfig { + public: + FeatureConfig(); + FeatureConfig(const FeatureConfig& other); + ~FeatureConfig(); + + // Whether the configuration has been successfully parsed. + bool valid; + + // The configuration for a particular event that will be searched for when + // counting how many times a particular feature has been used. + EventConfig used; + + // The configuration for a particular event that will be searched for when + // counting how many times in-product help has been triggered for a particular + // feature. + EventConfig trigger; + + // A set of all event configurations. + std::set<EventConfig> event_configs; + + // Number of in-product help triggered within this session must fit this + // comparison. + Comparator session_rate; + + // Number of days the in-product help has been available must fit this + // comparison. + Comparator availability; +}; + +bool operator==(const FeatureConfig& lhs, const FeatureConfig& rhs); +std::ostream& operator<<(std::ostream& os, const FeatureConfig& feature_config); + +// A Configuration contains the current set of runtime configurations. +// It is up to each implementation of Configuration to provide a way to +// register features and their configurations. +class Configuration { + public: + // Convenience alias for typical implementations of Configuration. + using ConfigMap = std::map<std::string, FeatureConfig>; + + virtual ~Configuration() = default; + + // Returns the FeatureConfig for the given |feature|. The |feature| must + // be registered with the Configuration instance. + virtual const FeatureConfig& GetFeatureConfig( + const base::Feature& feature) const = 0; + + // Returns the FeatureConfig for the given |feature|. The |feature_name| must + // be registered with the Configuration instance. + virtual const FeatureConfig& GetFeatureConfigByName( + const std::string& feature_name) const = 0; + + // Returns the immutable ConfigMap that contains all registered features. + virtual const ConfigMap& GetRegisteredFeatures() const = 0; + + protected: + Configuration() = default; + + private: + DISALLOW_COPY_AND_ASSIGN(Configuration); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_CONFIGURATION_H_ diff --git a/chromium/components/feature_engagement/internal/configuration_unittest.cc b/chromium/components/feature_engagement/internal/configuration_unittest.cc new file mode 100644 index 00000000000..540950e4918 --- /dev/null +++ b/chromium/components/feature_engagement/internal/configuration_unittest.cc @@ -0,0 +1,81 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/configuration.h" + +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +TEST(ComparatorTest, Any) { + EXPECT_TRUE(Comparator(ANY, 0).MeetsCriteria(0)); + EXPECT_TRUE(Comparator(ANY, 1).MeetsCriteria(0)); + EXPECT_TRUE(Comparator(ANY, 1).MeetsCriteria(1)); + EXPECT_TRUE(Comparator(ANY, 1).MeetsCriteria(2)); + EXPECT_TRUE(Comparator(ANY, 10).MeetsCriteria(9)); + EXPECT_TRUE(Comparator(ANY, 10).MeetsCriteria(10)); + EXPECT_TRUE(Comparator(ANY, 10).MeetsCriteria(11)); +} + +TEST(ComparatorTest, LessThan) { + EXPECT_FALSE(Comparator(LESS_THAN, 0).MeetsCriteria(0)); + EXPECT_TRUE(Comparator(LESS_THAN, 1).MeetsCriteria(0)); + EXPECT_FALSE(Comparator(LESS_THAN, 1).MeetsCriteria(1)); + EXPECT_FALSE(Comparator(LESS_THAN, 1).MeetsCriteria(2)); + EXPECT_TRUE(Comparator(LESS_THAN, 10).MeetsCriteria(9)); + EXPECT_FALSE(Comparator(LESS_THAN, 10).MeetsCriteria(10)); + EXPECT_FALSE(Comparator(LESS_THAN, 10).MeetsCriteria(11)); +} + +TEST(ComparatorTest, GreaterThan) { + EXPECT_FALSE(Comparator(GREATER_THAN, 0).MeetsCriteria(0)); + EXPECT_FALSE(Comparator(GREATER_THAN, 1).MeetsCriteria(0)); + EXPECT_FALSE(Comparator(GREATER_THAN, 1).MeetsCriteria(1)); + EXPECT_TRUE(Comparator(GREATER_THAN, 1).MeetsCriteria(2)); + EXPECT_FALSE(Comparator(GREATER_THAN, 10).MeetsCriteria(9)); + EXPECT_FALSE(Comparator(GREATER_THAN, 10).MeetsCriteria(10)); + EXPECT_TRUE(Comparator(GREATER_THAN, 10).MeetsCriteria(11)); +} + +TEST(ComparatorTest, LessThanOrEqual) { + EXPECT_TRUE(Comparator(LESS_THAN_OR_EQUAL, 0).MeetsCriteria(0)); + EXPECT_TRUE(Comparator(LESS_THAN_OR_EQUAL, 1).MeetsCriteria(0)); + EXPECT_TRUE(Comparator(LESS_THAN_OR_EQUAL, 1).MeetsCriteria(1)); + EXPECT_FALSE(Comparator(LESS_THAN_OR_EQUAL, 1).MeetsCriteria(2)); + EXPECT_TRUE(Comparator(LESS_THAN_OR_EQUAL, 10).MeetsCriteria(9)); + EXPECT_TRUE(Comparator(LESS_THAN_OR_EQUAL, 10).MeetsCriteria(10)); + EXPECT_FALSE(Comparator(LESS_THAN_OR_EQUAL, 10).MeetsCriteria(11)); +} + +TEST(ComparatorTest, GreaterThanOrEqual) { + EXPECT_TRUE(Comparator(GREATER_THAN_OR_EQUAL, 0).MeetsCriteria(0)); + EXPECT_FALSE(Comparator(GREATER_THAN_OR_EQUAL, 1).MeetsCriteria(0)); + EXPECT_TRUE(Comparator(GREATER_THAN_OR_EQUAL, 1).MeetsCriteria(1)); + EXPECT_TRUE(Comparator(GREATER_THAN_OR_EQUAL, 1).MeetsCriteria(2)); + EXPECT_FALSE(Comparator(GREATER_THAN_OR_EQUAL, 10).MeetsCriteria(9)); + EXPECT_TRUE(Comparator(GREATER_THAN_OR_EQUAL, 10).MeetsCriteria(10)); + EXPECT_TRUE(Comparator(GREATER_THAN_OR_EQUAL, 10).MeetsCriteria(11)); +} + +TEST(ComparatorTest, Equal) { + EXPECT_TRUE(Comparator(EQUAL, 0).MeetsCriteria(0)); + EXPECT_FALSE(Comparator(EQUAL, 1).MeetsCriteria(0)); + EXPECT_TRUE(Comparator(EQUAL, 1).MeetsCriteria(1)); + EXPECT_FALSE(Comparator(EQUAL, 1).MeetsCriteria(2)); + EXPECT_FALSE(Comparator(EQUAL, 10).MeetsCriteria(9)); + EXPECT_TRUE(Comparator(EQUAL, 10).MeetsCriteria(10)); + EXPECT_FALSE(Comparator(EQUAL, 10).MeetsCriteria(11)); +} + +TEST(ComparatorTest, NotEqual) { + EXPECT_FALSE(Comparator(NOT_EQUAL, 0).MeetsCriteria(0)); + EXPECT_TRUE(Comparator(NOT_EQUAL, 1).MeetsCriteria(0)); + EXPECT_FALSE(Comparator(NOT_EQUAL, 1).MeetsCriteria(1)); + EXPECT_TRUE(Comparator(NOT_EQUAL, 1).MeetsCriteria(2)); + EXPECT_TRUE(Comparator(NOT_EQUAL, 10).MeetsCriteria(9)); + EXPECT_FALSE(Comparator(NOT_EQUAL, 10).MeetsCriteria(10)); + EXPECT_TRUE(Comparator(NOT_EQUAL, 10).MeetsCriteria(11)); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/editable_configuration.cc b/chromium/components/feature_engagement/internal/editable_configuration.cc new file mode 100644 index 00000000000..cdce5386a2f --- /dev/null +++ b/chromium/components/feature_engagement/internal/editable_configuration.cc @@ -0,0 +1,44 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/editable_configuration.h" + +#include <map> + +#include "base/feature_list.h" +#include "base/logging.h" +#include "components/feature_engagement/internal/configuration.h" + +namespace feature_engagement { + +EditableConfiguration::EditableConfiguration() = default; + +EditableConfiguration::~EditableConfiguration() = default; + +void EditableConfiguration::SetConfiguration( + const base::Feature* feature, + const FeatureConfig& feature_config) { + configs_[feature->name] = feature_config; +} + +const FeatureConfig& EditableConfiguration::GetFeatureConfig( + const base::Feature& feature) const { + auto it = configs_.find(feature.name); + DCHECK(it != configs_.end()); + return it->second; +} + +const FeatureConfig& EditableConfiguration::GetFeatureConfigByName( + const std::string& feature_name) const { + auto it = configs_.find(feature_name); + DCHECK(it != configs_.end()); + return it->second; +} + +const Configuration::ConfigMap& EditableConfiguration::GetRegisteredFeatures() + const { + return configs_; +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/editable_configuration.h b/chromium/components/feature_engagement/internal/editable_configuration.h new file mode 100644 index 00000000000..b92b3da5140 --- /dev/null +++ b/chromium/components/feature_engagement/internal/editable_configuration.h @@ -0,0 +1,46 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_EDITABLE_CONFIGURATION_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_EDITABLE_CONFIGURATION_H_ + +#include "base/macros.h" +#include "components/feature_engagement/internal/configuration.h" + +namespace base { +struct Feature; +} // namespace base + +namespace feature_engagement { + +// An EditableConfiguration provides a configuration that can be configured +// by calling SetConfiguration(...) for each feature, which makes it well +// suited for simple setup and tests. +class EditableConfiguration : public Configuration { + public: + EditableConfiguration(); + ~EditableConfiguration() override; + + // Configuration implementation. + const FeatureConfig& GetFeatureConfig( + const base::Feature& feature) const override; + const FeatureConfig& GetFeatureConfigByName( + const std::string& feature_name) const override; + const Configuration::ConfigMap& GetRegisteredFeatures() const override; + + // Adds a new FeatureConfig to the current configurations. If it already + // exists, the contents are replaced. + void SetConfiguration(const base::Feature* feature, + const FeatureConfig& feature_config); + + private: + // The current configurations. + ConfigMap configs_; + + DISALLOW_COPY_AND_ASSIGN(EditableConfiguration); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_EDITABLE_CONFIGURATION_H_ diff --git a/chromium/components/feature_engagement/internal/editable_configuration_unittest.cc b/chromium/components/feature_engagement/internal/editable_configuration_unittest.cc new file mode 100644 index 00000000000..817d9881c9d --- /dev/null +++ b/chromium/components/feature_engagement/internal/editable_configuration_unittest.cc @@ -0,0 +1,77 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/editable_configuration.h" + +#include <string> + +#include "base/feature_list.h" +#include "components/feature_engagement/internal/configuration.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +namespace { + +const base::Feature kTestFeatureFoo{"test_foo", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureBar{"test_bar", + base::FEATURE_DISABLED_BY_DEFAULT}; + +class EditableConfigurationTest : public ::testing::Test { + public: + FeatureConfig CreateFeatureConfig(const std::string& feature_used_event, + bool valid) { + FeatureConfig feature_config; + feature_config.valid = valid; + feature_config.used.name = feature_used_event; + return feature_config; + } + + protected: + EditableConfiguration configuration_; +}; + +} // namespace + +TEST_F(EditableConfigurationTest, SingleConfigAddAndGet) { + FeatureConfig foo_config = CreateFeatureConfig("foo", true); + configuration_.SetConfiguration(&kTestFeatureFoo, foo_config); + const FeatureConfig& foo_config_result = + configuration_.GetFeatureConfig(kTestFeatureFoo); + + EXPECT_EQ(foo_config, foo_config_result); +} + +TEST_F(EditableConfigurationTest, TwoConfigAddAndGet) { + FeatureConfig foo_config = CreateFeatureConfig("foo", true); + configuration_.SetConfiguration(&kTestFeatureFoo, foo_config); + FeatureConfig bar_config = CreateFeatureConfig("bar", true); + configuration_.SetConfiguration(&kTestFeatureBar, bar_config); + + const FeatureConfig& foo_config_result = + configuration_.GetFeatureConfig(kTestFeatureFoo); + const FeatureConfig& bar_config_result = + configuration_.GetFeatureConfig(kTestFeatureBar); + + EXPECT_EQ(foo_config, foo_config_result); + EXPECT_EQ(bar_config, bar_config_result); +} + +TEST_F(EditableConfigurationTest, ConfigShouldBeEditable) { + FeatureConfig valid_foo_config = CreateFeatureConfig("foo", true); + configuration_.SetConfiguration(&kTestFeatureFoo, valid_foo_config); + + const FeatureConfig& valid_foo_config_result = + configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_EQ(valid_foo_config, valid_foo_config_result); + + FeatureConfig invalid_foo_config = CreateFeatureConfig("foo2", false); + configuration_.SetConfiguration(&kTestFeatureFoo, invalid_foo_config); + const FeatureConfig& invalid_foo_config_result = + configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_EQ(invalid_foo_config, invalid_foo_config_result); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/event_model.h b/chromium/components/feature_engagement/internal/event_model.h new file mode 100644 index 00000000000..5a2df54768f --- /dev/null +++ b/chromium/components/feature_engagement/internal/event_model.h @@ -0,0 +1,56 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_EVENT_MODEL_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_EVENT_MODEL_H_ + +#include <map> +#include <string> + +#include "base/callback.h" +#include "base/macros.h" + +namespace feature_engagement { +class Event; + +// A EventModel provides all necessary runtime state. +class EventModel { + public: + // Callback for when model initialization has finished. The |success| + // argument denotes whether the model was successfully initialized. + using OnModelInitializationFinished = base::Callback<void(bool success)>; + + virtual ~EventModel() = default; + + // Initialize the model, including all underlying sub systems. When all + // required operations have been finished, a callback is posted. + virtual void Initialize(const OnModelInitializationFinished& callback, + uint32_t current_day) = 0; + + // Returns whether the model is ready, i.e. whether it has been successfully + // initialized. + virtual bool IsReady() const = 0; + + // Retrieves the Event object for the event with the given name. If the event + // is not found, a nullptr will be returned. Calling this before the + // EventModel has finished initializing will result in undefined behavior. + virtual const Event* GetEvent(const std::string& event_name) const = 0; + + // Increments the counter for today for how many times the event has happened. + // If the event has never happened before, the Event object will be created. + // The |current_day| should be the number of days since UNIX epoch (see + // TimeProvider::GetCurrentDay()). + virtual void IncrementEvent(const std::string& event_name, + uint32_t current_day) = 0; + + protected: + EventModel() = default; + + private: + DISALLOW_COPY_AND_ASSIGN(EventModel); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_EVENT_MODEL_H_ diff --git a/chromium/components/feature_engagement/internal/event_model_impl.cc b/chromium/components/feature_engagement/internal/event_model_impl.cc new file mode 100644 index 00000000000..2fd365c9c5a --- /dev/null +++ b/chromium/components/feature_engagement/internal/event_model_impl.cc @@ -0,0 +1,135 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/event_model_impl.h" + +#include <map> +#include <memory> +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/logging.h" +#include "base/memory/weak_ptr.h" +#include "base/sequenced_task_runner.h" +#include "base/single_thread_task_runner.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/feature_engagement/internal/event_model.h" +#include "components/feature_engagement/internal/event_storage_validator.h" +#include "components/feature_engagement/internal/event_store.h" + +namespace feature_engagement { + +EventModelImpl::EventModelImpl( + std::unique_ptr<EventStore> store, + std::unique_ptr<EventStorageValidator> storage_validator) + : EventModel(), + store_(std::move(store)), + storage_validator_(std::move(storage_validator)), + ready_(false), + weak_factory_(this) {} + +EventModelImpl::~EventModelImpl() = default; + +void EventModelImpl::Initialize(const OnModelInitializationFinished& callback, + uint32_t current_day) { + store_->Load(base::Bind(&EventModelImpl::OnStoreLoaded, + weak_factory_.GetWeakPtr(), callback, current_day)); +} + +bool EventModelImpl::IsReady() const { + return ready_; +} + +const Event* EventModelImpl::GetEvent(const std::string& event_name) const { + auto search = events_.find(event_name); + if (search == events_.end()) + return nullptr; + + return &search->second; +} + +void EventModelImpl::IncrementEvent(const std::string& event_name, + uint32_t current_day) { + DCHECK(ready_); + + if (!storage_validator_->ShouldStore(event_name)) { + DVLOG(2) << "Not incrementing event " << event_name << " @ " << current_day; + return; + } + + DVLOG(2) << "Incrementing event " << event_name << " @ " << current_day; + + Event& event = GetNonConstEvent(event_name); + for (int i = 0; i < event.events_size(); ++i) { + Event_Count* event_count = event.mutable_events(i); + DCHECK(event_count->has_day()); + DCHECK(event_count->has_count()); + if (event_count->day() == current_day) { + event_count->set_count(event_count->count() + 1); + store_->WriteEvent(event); + return; + } + } + + // Day not found for event, adding new day with a count of 1. + Event_Count* event_count = event.add_events(); + event_count->set_day(current_day); + event_count->set_count(1u); + store_->WriteEvent(event); +} + +void EventModelImpl::OnStoreLoaded( + const OnModelInitializationFinished& callback, + uint32_t current_day, + bool success, + std::unique_ptr<std::vector<Event>> events) { + if (!success) { + callback.Run(false); + return; + } + + for (auto& event : *events) { + DCHECK_NE("", event.name()); + + Event new_event; + for (const auto& event_count : event.events()) { + if (!storage_validator_->ShouldKeep(event.name(), event_count.day(), + current_day)) { + continue; + } + + Event_Count* new_event_count = new_event.add_events(); + new_event_count->set_day(event_count.day()); + new_event_count->set_count(event_count.count()); + } + + // Only keep Event object that have days with activity. + if (new_event.events_size() > 0) { + new_event.set_name(event.name()); + events_[event.name()] = new_event; + + // If the number of events is not the same, overwrite DB entry. + if (new_event.events_size() != event.events_size()) + store_->WriteEvent(new_event); + } else { + // If there are no more activity for an Event, delete the whole event. + store_->DeleteEvent(event.name()); + } + } + + ready_ = true; + callback.Run(true); +} + +Event& EventModelImpl::GetNonConstEvent(const std::string& event_name) { + if (events_.find(event_name) == events_.end()) { + // Event does not exist yet, so create it. + events_[event_name].set_name(event_name); + store_->WriteEvent(events_[event_name]); + } + return events_[event_name]; +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/event_model_impl.h b/chromium/components/feature_engagement/internal/event_model_impl.h new file mode 100644 index 00000000000..1c70bddcc3a --- /dev/null +++ b/chromium/components/feature_engagement/internal/event_model_impl.h @@ -0,0 +1,68 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_EVENT_MODEL_IMPL_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_EVENT_MODEL_IMPL_H_ + +#include <map> +#include <memory> +#include <string> +#include <vector> + +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "components/feature_engagement/internal/event_model.h" +#include "components/feature_engagement/internal/proto/event.pb.h" + +namespace feature_engagement { +class EventStorageValidator; +class EventStore; + +// A EventModelImpl provides the default implementation of the EventModel. +class EventModelImpl : public EventModel { + public: + EventModelImpl(std::unique_ptr<EventStore> store, + std::unique_ptr<EventStorageValidator> storage_validator); + ~EventModelImpl() override; + + // EventModel implementation. + void Initialize(const OnModelInitializationFinished& callback, + uint32_t current_day) override; + bool IsReady() const override; + const Event* GetEvent(const std::string& event_name) const override; + void IncrementEvent(const std::string& event_name, + uint32_t current_day) override; + + private: + // Callback for loading the underlying store. + void OnStoreLoaded(const OnModelInitializationFinished& callback, + uint32_t current_day, + bool success, + std::unique_ptr<std::vector<Event>> events); + + // Internal version for getting the non-const version of a stored Event. + // Creates the event if it is not already stored. + Event& GetNonConstEvent(const std::string& event_name); + + // The underlying store for all events. + std::unique_ptr<EventStore> store_; + + // A utility for checking whether new events should be stored and for whether + // old events should be kept. + std::unique_ptr<EventStorageValidator> storage_validator_; + + // An in-memory representation of all events. + std::map<std::string, Event> events_; + + // Whether the model has been fully initialized. + bool ready_; + + base::WeakPtrFactory<EventModelImpl> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(EventModelImpl); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_EVENT_MODEL_IMPL_H_ diff --git a/chromium/components/feature_engagement/internal/event_model_impl_unittest.cc b/chromium/components/feature_engagement/internal/event_model_impl_unittest.cc new file mode 100644 index 00000000000..818dd48042e --- /dev/null +++ b/chromium/components/feature_engagement/internal/event_model_impl_unittest.cc @@ -0,0 +1,467 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/event_model_impl.h" + +#include <memory> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/feature_list.h" +#include "base/memory/ptr_util.h" +#include "base/test/test_simple_task_runner.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/feature_engagement/internal/editable_configuration.h" +#include "components/feature_engagement/internal/in_memory_event_store.h" +#include "components/feature_engagement/internal/never_event_storage_validator.h" +#include "components/feature_engagement/internal/proto/event.pb.h" +#include "components/feature_engagement/internal/test/event_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +namespace { + +// A test-only implementation of InMemoryEventStore that tracks calls to +// WriteEvent(...). +class TestInMemoryEventStore : public InMemoryEventStore { + public: + TestInMemoryEventStore(std::unique_ptr<std::vector<Event>> events, + bool load_should_succeed) + : InMemoryEventStore(std::move(events)), + store_operation_count_(0), + load_should_succeed_(load_should_succeed) {} + + void Load(const OnLoadedCallback& callback) override { + HandleLoadResult(callback, load_should_succeed_); + } + + void WriteEvent(const Event& event) override { + ++store_operation_count_; + last_written_event_.reset(new Event(event)); + } + + void DeleteEvent(const std::string& event_name) override { + ++store_operation_count_; + last_deleted_event_ = event_name; + } + + const Event* GetLastWrittenEvent() { return last_written_event_.get(); } + + const std::string GetLastDeletedEvent() { return last_deleted_event_; } + + uint32_t GetStoreOperationCount() { return store_operation_count_; } + + private: + // Temporary store the last written event. + std::unique_ptr<Event> last_written_event_; + + // Temporary store the last deleted event. + std::string last_deleted_event_; + + // Tracks the number of operations performed on the store. + uint32_t store_operation_count_; + + // Denotes whether the call to Load(...) should succeed or not. This impacts + // both the ready-state and the result for the OnLoadedCallback. + bool load_should_succeed_; +}; + +class TestEventStorageValidator : public EventStorageValidator { + public: + TestEventStorageValidator() : should_store_(true) {} + + bool ShouldStore(const std::string& event_name) const override { + return should_store_; + } + + bool ShouldKeep(const std::string& event_name, + uint32_t event_day, + uint32_t current_day) const override { + auto search = max_keep_ages_.find(event_name); + if (search == max_keep_ages_.end()) + return false; + + return (current_day - event_day) < search->second; + } + + void SetShouldStore(bool should_store) { should_store_ = should_store; } + + void SetMaxKeepAge(const std::string& event_name, uint32_t age) { + max_keep_ages_[event_name] = age; + } + + private: + bool should_store_; + std::map<std::string, uint32_t> max_keep_ages_; + + DISALLOW_COPY_AND_ASSIGN(TestEventStorageValidator); +}; + +// Creates a TestInMemoryEventStore containing three hard coded events. +std::unique_ptr<TestInMemoryEventStore> CreatePrefilledStore() { + std::unique_ptr<std::vector<Event>> events = + base::MakeUnique<std::vector<Event>>(); + + Event foo; + foo.set_name("foo"); + test::SetEventCountForDay(&foo, 1, 1); + events->push_back(foo); + + Event bar; + bar.set_name("bar"); + test::SetEventCountForDay(&bar, 1, 3); + test::SetEventCountForDay(&bar, 2, 3); + test::SetEventCountForDay(&bar, 5, 5); + events->push_back(bar); + + Event qux; + qux.set_name("qux"); + test::SetEventCountForDay(&qux, 1, 5); + test::SetEventCountForDay(&qux, 2, 1); + test::SetEventCountForDay(&qux, 3, 2); + events->push_back(qux); + + return base::MakeUnique<TestInMemoryEventStore>(std::move(events), true); +} + +class EventModelImplTest : public ::testing::Test { + public: + EventModelImplTest() + : task_runner_(new base::TestSimpleTaskRunner), + handle_(task_runner_), + got_initialize_callback_(false), + initialize_callback_result_(false) {} + + void SetUp() override { + std::unique_ptr<TestInMemoryEventStore> store = CreateStore(); + store_ = store.get(); + + auto storage_validator = base::MakeUnique<TestEventStorageValidator>(); + storage_validator_ = storage_validator.get(); + + model_.reset( + new EventModelImpl(std::move(store), std::move(storage_validator))); + + // By default store all events for a very long time. + storage_validator_->SetMaxKeepAge("foo", 10000u); + storage_validator_->SetMaxKeepAge("bar", 10000u); + storage_validator_->SetMaxKeepAge("qux", 10000u); + } + + virtual std::unique_ptr<TestInMemoryEventStore> CreateStore() { + return CreatePrefilledStore(); + } + + void OnModelInitializationFinished(bool success) { + got_initialize_callback_ = true; + initialize_callback_result_ = success; + } + + protected: + scoped_refptr<base::TestSimpleTaskRunner> task_runner_; + base::ThreadTaskRunnerHandle handle_; + + std::unique_ptr<EventModelImpl> model_; + TestInMemoryEventStore* store_; + TestEventStorageValidator* storage_validator_; + bool got_initialize_callback_; + bool initialize_callback_result_; +}; + +class LoadFailingEventModelImplTest : public EventModelImplTest { + public: + LoadFailingEventModelImplTest() : EventModelImplTest() {} + + std::unique_ptr<TestInMemoryEventStore> CreateStore() override { + return base::MakeUnique<TestInMemoryEventStore>( + base::MakeUnique<std::vector<Event>>(), false); + } +}; + +} // namespace + +TEST_F(EventModelImplTest, InitializeShouldBeReadyImmediatelyAfterCallback) { + model_->Initialize( + base::Bind(&EventModelImplTest::OnModelInitializationFinished, + base::Unretained(this)), + 1000u); + + // Only run pending tasks on the queue. Do not run any subsequently queued + // tasks that result from running the current pending tasks. + task_runner_->RunPendingTasks(); + + EXPECT_TRUE(got_initialize_callback_); + EXPECT_TRUE(model_->IsReady()); +} + +TEST_F(EventModelImplTest, InitializeShouldLoadEntries) { + model_->Initialize( + base::Bind(&EventModelImplTest::OnModelInitializationFinished, + base::Unretained(this)), + 1000u); + task_runner_->RunUntilIdle(); + EXPECT_TRUE(model_->IsReady()); + EXPECT_TRUE(got_initialize_callback_); + EXPECT_TRUE(initialize_callback_result_); + + // Verify that all the data matches what was put into the store in + // CreateStore(). + const Event* foo_event = model_->GetEvent("foo"); + EXPECT_EQ("foo", foo_event->name()); + EXPECT_EQ(1, foo_event->events_size()); + test::VerifyEventCount(foo_event, 1u, 1u); + + const Event* bar_event = model_->GetEvent("bar"); + EXPECT_EQ("bar", bar_event->name()); + EXPECT_EQ(3, bar_event->events_size()); + test::VerifyEventCount(bar_event, 1u, 3u); + test::VerifyEventCount(bar_event, 2u, 3u); + test::VerifyEventCount(bar_event, 5u, 5u); + + const Event* qux_event = model_->GetEvent("qux"); + EXPECT_EQ("qux", qux_event->name()); + EXPECT_EQ(3, qux_event->events_size()); + test::VerifyEventCount(qux_event, 1u, 5u); + test::VerifyEventCount(qux_event, 2u, 1u); + test::VerifyEventCount(qux_event, 3u, 2u); +} + +TEST_F(EventModelImplTest, InitializeShouldOnlyLoadEntriesThatShouldBeKept) { + // Back to day 5, i.e. no entries. + storage_validator_->SetMaxKeepAge("foo", 1u); + + // Back to day 2, i.e. 2 events. + storage_validator_->SetMaxKeepAge("bar", 4u); + + // Back to day epoch, i.e. all events. + storage_validator_->SetMaxKeepAge("qux", 10u); + + model_->Initialize( + base::Bind(&EventModelImplTest::OnModelInitializationFinished, + base::Unretained(this)), + 5u); + task_runner_->RunUntilIdle(); + EXPECT_TRUE(model_->IsReady()); + EXPECT_TRUE(got_initialize_callback_); + EXPECT_TRUE(initialize_callback_result_); + + // Verify that all the data matches what was put into the store in + // CreateStore(), minus the events that should no longer exist. + const Event* foo_event = model_->GetEvent("foo"); + EXPECT_EQ(nullptr, foo_event); + EXPECT_EQ("foo", store_->GetLastDeletedEvent()); + + const Event* bar_event = model_->GetEvent("bar"); + EXPECT_EQ("bar", bar_event->name()); + EXPECT_EQ(2, bar_event->events_size()); + test::VerifyEventCount(bar_event, 2u, 3u); + test::VerifyEventCount(bar_event, 5u, 5u); + test::VerifyEventsEqual(bar_event, store_->GetLastWrittenEvent()); + + // Nothing has changed for 'qux', so nothing will be written to EventStore. + const Event* qux_event = model_->GetEvent("qux"); + EXPECT_EQ("qux", qux_event->name()); + EXPECT_EQ(3, qux_event->events_size()); + test::VerifyEventCount(qux_event, 1u, 5u); + test::VerifyEventCount(qux_event, 2u, 1u); + test::VerifyEventCount(qux_event, 3u, 2u); + + // In total, only two operations should have happened, the update of "bar", + // and the delete of "foo". + EXPECT_EQ(2u, store_->GetStoreOperationCount()); +} + +TEST_F(EventModelImplTest, RetrievingNewEventsShouldYieldNullptr) { + model_->Initialize( + base::Bind(&EventModelImplTest::OnModelInitializationFinished, + base::Unretained(this)), + 1000u); + task_runner_->RunUntilIdle(); + EXPECT_TRUE(model_->IsReady()); + + const Event* no_event = model_->GetEvent("no"); + EXPECT_EQ(nullptr, no_event); + test::VerifyEventsEqual(nullptr, store_->GetLastWrittenEvent()); +} + +TEST_F(EventModelImplTest, IncrementingNonExistingEvent) { + model_->Initialize( + base::Bind(&EventModelImplTest::OnModelInitializationFinished, + base::Unretained(this)), + 1000u); + task_runner_->RunUntilIdle(); + EXPECT_TRUE(model_->IsReady()); + + // Incrementing the event should work even if it does not exist. + model_->IncrementEvent("nonexisting", 1u); + const Event* event1 = model_->GetEvent("nonexisting"); + ASSERT_NE(nullptr, event1); + EXPECT_EQ("nonexisting", event1->name()); + EXPECT_EQ(1, event1->events_size()); + test::VerifyEventCount(event1, 1u, 1u); + test::VerifyEventsEqual(event1, store_->GetLastWrittenEvent()); + + // Incrementing the event after it has been initialized to 1, it should now + // have a count of 2 for the given day. + model_->IncrementEvent("nonexisting", 1u); + const Event* event2 = model_->GetEvent("nonexisting"); + ASSERT_NE(nullptr, event2); + Event_Count event2_count = event2->events(0); + EXPECT_EQ(1, event2->events_size()); + test::VerifyEventCount(event2, 1u, 2u); + test::VerifyEventsEqual(event2, store_->GetLastWrittenEvent()); +} + +TEST_F(EventModelImplTest, IncrementingNonExistingEventMultipleDays) { + model_->Initialize( + base::Bind(&EventModelImplTest::OnModelInitializationFinished, + base::Unretained(this)), + 1000u); + task_runner_->RunUntilIdle(); + EXPECT_TRUE(model_->IsReady()); + + model_->IncrementEvent("nonexisting", 1u); + model_->IncrementEvent("nonexisting", 2u); + model_->IncrementEvent("nonexisting", 2u); + model_->IncrementEvent("nonexisting", 3u); + const Event* event = model_->GetEvent("nonexisting"); + ASSERT_NE(nullptr, event); + EXPECT_EQ(3, event->events_size()); + test::VerifyEventCount(event, 1u, 1u); + test::VerifyEventCount(event, 2u, 2u); + test::VerifyEventCount(event, 3u, 1u); + test::VerifyEventsEqual(event, store_->GetLastWrittenEvent()); +} + +TEST_F(EventModelImplTest, IncrementingNonExistingEventWithoutStoring) { + model_->Initialize( + base::Bind(&EventModelImplTest::OnModelInitializationFinished, + base::Unretained(this)), + 1000u); + task_runner_->RunUntilIdle(); + EXPECT_TRUE(model_->IsReady()); + + storage_validator_->SetShouldStore(false); + + // Incrementing the event should not be written or stored in-memory. + model_->IncrementEvent("nonexisting", 1u); + const Event* event1 = model_->GetEvent("nonexisting"); + EXPECT_EQ(nullptr, event1); + test::VerifyEventsEqual(nullptr, store_->GetLastWrittenEvent()); +} + +TEST_F(EventModelImplTest, IncrementingExistingEventWithoutStoring) { + model_->Initialize( + base::Bind(&EventModelImplTest::OnModelInitializationFinished, + base::Unretained(this)), + 1000u); + task_runner_->RunUntilIdle(); + EXPECT_TRUE(model_->IsReady()); + + // Write one event before turning off storage. + model_->IncrementEvent("nonexisting", 1u); + const Event* first_event = model_->GetEvent("nonexisting"); + ASSERT_NE(nullptr, first_event); + test::VerifyEventsEqual(first_event, store_->GetLastWrittenEvent()); + + storage_validator_->SetShouldStore(false); + + // Incrementing the event should no longer be written or stored in-memory. + model_->IncrementEvent("nonexisting", 1u); + const Event* second_event = model_->GetEvent("nonexisting"); + EXPECT_EQ(first_event, second_event); + test::VerifyEventsEqual(first_event, store_->GetLastWrittenEvent()); +} + +TEST_F(EventModelImplTest, IncrementingSingleDayExistingEvent) { + model_->Initialize( + base::Bind(&EventModelImplTest::OnModelInitializationFinished, + base::Unretained(this)), + 1000u); + task_runner_->RunUntilIdle(); + EXPECT_TRUE(model_->IsReady()); + + // |foo| is inserted into the store with a count of 1 at day 1. + const Event* foo_event = model_->GetEvent("foo"); + EXPECT_EQ("foo", foo_event->name()); + EXPECT_EQ(1, foo_event->events_size()); + test::VerifyEventCount(foo_event, 1u, 1u); + + // Incrementing |foo| should change count to 2. + model_->IncrementEvent("foo", 1u); + const Event* foo_event2 = model_->GetEvent("foo"); + EXPECT_EQ(1, foo_event2->events_size()); + test::VerifyEventCount(foo_event2, 1u, 2u); + test::VerifyEventsEqual(foo_event2, store_->GetLastWrittenEvent()); +} + +TEST_F(EventModelImplTest, IncrementingSingleDayExistingEventTwice) { + model_->Initialize( + base::Bind(&EventModelImplTest::OnModelInitializationFinished, + base::Unretained(this)), + 1000u); + task_runner_->RunUntilIdle(); + EXPECT_TRUE(model_->IsReady()); + + // |foo| is inserted into the store with a count of 1 at day 1, so + // incrementing twice should lead to 3. + model_->IncrementEvent("foo", 1u); + model_->IncrementEvent("foo", 1u); + const Event* foo_event = model_->GetEvent("foo"); + EXPECT_EQ(1, foo_event->events_size()); + test::VerifyEventCount(foo_event, 1u, 3u); + test::VerifyEventsEqual(foo_event, store_->GetLastWrittenEvent()); +} + +TEST_F(EventModelImplTest, IncrementingExistingMultiDayEvent) { + model_->Initialize( + base::Bind(&EventModelImplTest::OnModelInitializationFinished, + base::Unretained(this)), + 1000u); + task_runner_->RunUntilIdle(); + EXPECT_TRUE(model_->IsReady()); + + // |bar| is inserted into the store with a count of 3 at day 2. Incrementing + // that day should lead to a count of 4. + const Event* bar_event = model_->GetEvent("bar"); + test::VerifyEventCount(bar_event, 2u, 3u); + model_->IncrementEvent("bar", 2u); + const Event* bar_event2 = model_->GetEvent("bar"); + test::VerifyEventCount(bar_event2, 2u, 4u); + test::VerifyEventsEqual(bar_event2, store_->GetLastWrittenEvent()); +} + +TEST_F(EventModelImplTest, IncrementingExistingMultiDayEventNewDay) { + model_->Initialize( + base::Bind(&EventModelImplTest::OnModelInitializationFinished, + base::Unretained(this)), + 1000u); + task_runner_->RunUntilIdle(); + EXPECT_TRUE(model_->IsReady()); + + // |bar| does not contain entries for day 10, so incrementing should create + // the day. + model_->IncrementEvent("bar", 10u); + const Event* bar_event = model_->GetEvent("bar"); + test::VerifyEventCount(bar_event, 10u, 1u); + test::VerifyEventsEqual(bar_event, store_->GetLastWrittenEvent()); + model_->IncrementEvent("bar", 10u); + const Event* bar_event2 = model_->GetEvent("bar"); + test::VerifyEventCount(bar_event2, 10u, 2u); + test::VerifyEventsEqual(bar_event2, store_->GetLastWrittenEvent()); +} + +TEST_F(LoadFailingEventModelImplTest, FailedInitializeInformsCaller) { + model_->Initialize( + base::Bind(&EventModelImplTest::OnModelInitializationFinished, + base::Unretained(this)), + 1000u); + task_runner_->RunUntilIdle(); + EXPECT_FALSE(model_->IsReady()); + EXPECT_TRUE(got_initialize_callback_); + EXPECT_FALSE(initialize_callback_result_); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/event_storage_validator.h b/chromium/components/feature_engagement/internal/event_storage_validator.h new file mode 100644 index 00000000000..db41c843b74 --- /dev/null +++ b/chromium/components/feature_engagement/internal/event_storage_validator.h @@ -0,0 +1,40 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_EVENT_STORAGE_VALIDATOR_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_EVENT_STORAGE_VALIDATOR_H_ + +#include <string> + +#include "base/macros.h" + +namespace feature_engagement { + +// A EventStorageValidator checks the required storage conditions for a given +// event, and checks if all conditions are met for storing it. +class EventStorageValidator { + public: + virtual ~EventStorageValidator() = default; + + // Returns true iff new events of this type should be stored. + // This is typically called before storing each incoming event. + virtual bool ShouldStore(const std::string& event_name) const = 0; + + // Returns true iff events of this type should be kept for the given day. + // This is typically called during load of the internal database state, to + // possibly throw away old data. + virtual bool ShouldKeep(const std::string& event_name, + uint32_t event_day, + uint32_t current_day) const = 0; + + protected: + EventStorageValidator() = default; + + private: + DISALLOW_COPY_AND_ASSIGN(EventStorageValidator); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_EVENT_STORAGE_VALIDATOR_H_ diff --git a/chromium/components/feature_engagement/internal/event_store.h b/chromium/components/feature_engagement/internal/event_store.h new file mode 100644 index 00000000000..a70820ab8dd --- /dev/null +++ b/chromium/components/feature_engagement/internal/event_store.h @@ -0,0 +1,48 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_STORE_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_STORE_H_ + +#include <string> + +#include "base/callback.h" +#include "base/macros.h" +#include "components/feature_engagement/internal/proto/event.pb.h" + +namespace feature_engagement { + +// EventStore represents the storage engine behind the EventModel. +class EventStore { + public: + using OnLoadedCallback = + base::Callback<void(bool success, std::unique_ptr<std::vector<Event>>)>; + + virtual ~EventStore() = default; + + // Loads the database from storage and asynchronously posts the result back + // on the caller's thread. + // Ownership of the loaded data is given to the caller. + virtual void Load(const OnLoadedCallback& callback) = 0; + + // Returns whether the database is ready, i.e. whether it has been fully + // loaded. + virtual bool IsReady() const = 0; + + // Stores the given event to persistent storage. + virtual void WriteEvent(const Event& event) = 0; + + // Deletes the event with the given name. + virtual void DeleteEvent(const std::string& event_name) = 0; + + protected: + EventStore() = default; + + private: + DISALLOW_COPY_AND_ASSIGN(EventStore); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_STORE_H_ diff --git a/chromium/components/feature_engagement/internal/feature_config_condition_validator.cc b/chromium/components/feature_engagement/internal/feature_config_condition_validator.cc new file mode 100644 index 00000000000..2ec5b835153 --- /dev/null +++ b/chromium/components/feature_engagement/internal/feature_config_condition_validator.cc @@ -0,0 +1,122 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/feature_config_condition_validator.h" + +#include "base/feature_list.h" +#include "components/feature_engagement/internal/availability_model.h" +#include "components/feature_engagement/internal/configuration.h" +#include "components/feature_engagement/internal/event_model.h" +#include "components/feature_engagement/internal/proto/event.pb.h" +#include "components/feature_engagement/public/feature_list.h" + +namespace feature_engagement { + +FeatureConfigConditionValidator::FeatureConfigConditionValidator() + : currently_showing_(false), times_shown_(0u) {} + +FeatureConfigConditionValidator::~FeatureConfigConditionValidator() = default; + +ConditionValidator::Result FeatureConfigConditionValidator::MeetsConditions( + const base::Feature& feature, + const FeatureConfig& config, + const EventModel& event_model, + const AvailabilityModel& availability_model, + uint32_t current_day) const { + ConditionValidator::Result result(true); + result.event_model_ready_ok = event_model.IsReady(); + result.currently_showing_ok = !currently_showing_; + result.feature_enabled_ok = base::FeatureList::IsEnabled(feature); + result.config_ok = config.valid; + result.used_ok = + EventConfigMeetsConditions(config.used, event_model, current_day); + result.trigger_ok = + EventConfigMeetsConditions(config.trigger, event_model, current_day); + + for (const auto& event_config : config.event_configs) { + result.preconditions_ok &= + EventConfigMeetsConditions(event_config, event_model, current_day); + } + + result.session_rate_ok = config.session_rate.MeetsCriteria(times_shown_); + + result.availability_model_ready_ok = availability_model.IsReady(); + + result.availability_ok = AvailabilityMeetsConditions( + feature, config.availability, availability_model, current_day); + + return result; +} + +void FeatureConfigConditionValidator::NotifyIsShowing( + const base::Feature& feature) { + DCHECK(!currently_showing_); + DCHECK(base::FeatureList::IsEnabled(feature)); + + currently_showing_ = true; + ++times_shown_; +} + +void FeatureConfigConditionValidator::NotifyDismissed( + const base::Feature& feature) { + currently_showing_ = false; +} + +bool FeatureConfigConditionValidator::EventConfigMeetsConditions( + const EventConfig& event_config, + const EventModel& event_model, + uint32_t current_day) const { + const Event* event = event_model.GetEvent(event_config.name); + + // If no events are found, the requirement must be met with 0 elements. + // Also, if the window is 0 days, there will never be any events. + if (event == nullptr || event_config.window == 0u) + return event_config.comparator.MeetsCriteria(0u); + + DCHECK(event_config.window >= 0); + + // A window of N=0: Nothing should be counted. + // A window of N=1: |current_day| should be counted. + // A window of N=2+: |current_day| plus |N-1| more days should be counted. + uint32_t oldest_accepted_day = current_day - event_config.window + 1; + + // Cap |oldest_accepted_day| to UNIX epoch. + if (event_config.window > current_day) + oldest_accepted_day = 0u; + + // Calculate the number of events within the window. + uint32_t event_count = 0; + for (const auto& event_day : event->events()) { + if (event_day.day() < oldest_accepted_day) + continue; + + event_count += event_day.count(); + } + + return event_config.comparator.MeetsCriteria(event_count); +} + +bool FeatureConfigConditionValidator::AvailabilityMeetsConditions( + const base::Feature& feature, + Comparator comparator, + const AvailabilityModel& availability_model, + uint32_t current_day) const { + if (comparator.type == ANY) + return true; + + base::Optional<uint32_t> availability_day = + availability_model.GetAvailability(feature); + if (!availability_day.has_value()) + return false; + + uint32_t days_available = current_day - availability_day.value(); + + // Ensure that availability days never wrap around. + if (availability_day.value() > current_day) + days_available = 0u; + + return comparator.MeetsCriteria(days_available); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/feature_config_condition_validator.h b/chromium/components/feature_engagement/internal/feature_config_condition_validator.h new file mode 100644 index 00000000000..949eaa792bf --- /dev/null +++ b/chromium/components/feature_engagement/internal/feature_config_condition_validator.h @@ -0,0 +1,56 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_FEATURE_CONFIG_CONDITION_VALIDATOR_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_FEATURE_CONFIG_CONDITION_VALIDATOR_H_ + +#include <stdint.h> + +#include "base/macros.h" +#include "components/feature_engagement/internal/condition_validator.h" + +namespace feature_engagement { +class AvailabilityModel; +struct Comparator; +struct EventConfig; +class EventModel; + +// A ConditionValidator that uses the FeatureConfigs as the source of truth. +class FeatureConfigConditionValidator : public ConditionValidator { + public: + FeatureConfigConditionValidator(); + ~FeatureConfigConditionValidator() override; + + // ConditionValidator implementation. + ConditionValidator::Result MeetsConditions( + const base::Feature& feature, + const FeatureConfig& config, + const EventModel& event_model, + const AvailabilityModel& availability_model, + uint32_t current_day) const override; + void NotifyIsShowing(const base::Feature& feature) override; + void NotifyDismissed(const base::Feature& feature) override; + + private: + bool EventConfigMeetsConditions(const EventConfig& event_config, + const EventModel& event_model, + uint32_t current_day) const; + + bool AvailabilityMeetsConditions(const base::Feature& feature, + Comparator comparator, + const AvailabilityModel& availability_model, + uint32_t current_day) const; + + // Whether in-product help is currently being shown. + bool currently_showing_; + + // Number of times in-product help has been shown within the current session. + uint32_t times_shown_; + + DISALLOW_COPY_AND_ASSIGN(FeatureConfigConditionValidator); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_FEATURE_CONFIG_CONDITION_VALIDATOR_H_ diff --git a/chromium/components/feature_engagement/internal/feature_config_condition_validator_unittest.cc b/chromium/components/feature_engagement/internal/feature_config_condition_validator_unittest.cc new file mode 100644 index 00000000000..5f6420f8925 --- /dev/null +++ b/chromium/components/feature_engagement/internal/feature_config_condition_validator_unittest.cc @@ -0,0 +1,538 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/feature_config_condition_validator.h" + +#include <map> +#include <string> + +#include "base/feature_list.h" +#include "base/metrics/field_trial.h" +#include "base/test/scoped_feature_list.h" +#include "components/feature_engagement/internal/availability_model.h" +#include "components/feature_engagement/internal/configuration.h" +#include "components/feature_engagement/internal/event_model.h" +#include "components/feature_engagement/internal/proto/event.pb.h" +#include "components/feature_engagement/internal/test/event_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +namespace { + +const base::Feature kTestFeatureFoo{"test_foo", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureBar{"test_bar", + base::FEATURE_DISABLED_BY_DEFAULT}; + +FeatureConfig GetValidFeatureConfig() { + FeatureConfig config; + config.valid = true; + return config; +} + +FeatureConfig GetAcceptingFeatureConfig() { + FeatureConfig config; + config.valid = true; + config.used = EventConfig("used", Comparator(ANY, 0), 0, 0); + config.trigger = EventConfig("trigger", Comparator(ANY, 0), 0, 0); + config.session_rate = Comparator(ANY, 0); + config.availability = Comparator(ANY, 0); + return config; +} + +class TestEventModel : public EventModel { + public: + TestEventModel() : ready_(true) {} + + void Initialize(const OnModelInitializationFinished& callback, + uint32_t current_day) override {} + + bool IsReady() const override { return ready_; } + + void SetIsReady(bool ready) { ready_ = ready; } + + const Event* GetEvent(const std::string& event_name) const override { + auto search = events_.find(event_name); + if (search == events_.end()) + return nullptr; + + return &search->second; + } + + void SetEvent(const Event& event) { events_[event.name()] = event; } + + void IncrementEvent(const std::string& event_name, uint32_t day) override {} + + private: + std::map<std::string, Event> events_; + bool ready_; +}; + +class TestAvailabilityModel : public AvailabilityModel { + public: + TestAvailabilityModel() : ready_(true) {} + ~TestAvailabilityModel() override = default; + + void Initialize(AvailabilityModel::OnInitializedCallback callback, + uint32_t current_day) override {} + + bool IsReady() const override { return ready_; } + + void SetIsReady(bool ready) { ready_ = ready; } + + base::Optional<uint32_t> GetAvailability( + const base::Feature& feature) const override { + auto search = availabilities_.find(feature.name); + if (search == availabilities_.end()) + return base::nullopt; + + return search->second; + } + + void SetAvailability(const base::Feature* feature, + base::Optional<uint32_t> availability) { + availabilities_[feature->name] = availability; + } + + private: + bool ready_; + + std::map<std::string, base::Optional<uint32_t>> availabilities_; + + DISALLOW_COPY_AND_ASSIGN(TestAvailabilityModel); +}; + +class FeatureConfigConditionValidatorTest : public ::testing::Test { + public: + FeatureConfigConditionValidatorTest() = default; + + protected: + ConditionValidator::Result GetResultForDayAndEventWindow( + Comparator comparator, + uint32_t window, + uint32_t current_day) { + FeatureConfig config = GetAcceptingFeatureConfig(); + config.event_configs.insert(EventConfig("event1", comparator, window, 0)); + return validator_.MeetsConditions(kTestFeatureFoo, config, event_model_, + availability_model_, current_day); + } + + ConditionValidator::Result GetResultForDay(const FeatureConfig& config, + uint32_t current_day) { + return validator_.MeetsConditions(kTestFeatureFoo, config, event_model_, + availability_model_, current_day); + } + + ConditionValidator::Result GetResultForDayZero(const FeatureConfig& config) { + return validator_.MeetsConditions(kTestFeatureFoo, config, event_model_, + availability_model_, 0); + } + + TestEventModel event_model_; + TestAvailabilityModel availability_model_; + FeatureConfigConditionValidator validator_; + uint32_t current_day_; + + private: + DISALLOW_COPY_AND_ASSIGN(FeatureConfigConditionValidatorTest); +}; + +} // namespace + +TEST_F(FeatureConfigConditionValidatorTest, ModelNotReadyShouldFail) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo}, {}); + + event_model_.SetIsReady(false); + + ConditionValidator::Result result = + GetResultForDayZero(GetValidFeatureConfig()); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.event_model_ready_ok); +} + +TEST_F(FeatureConfigConditionValidatorTest, ConfigInvalidShouldFail) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo}, {}); + + ConditionValidator::Result result = GetResultForDayZero(FeatureConfig()); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.config_ok); +} + +TEST_F(FeatureConfigConditionValidatorTest, MultipleErrorsShouldBeSet) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo}, {}); + + event_model_.SetIsReady(false); + + ConditionValidator::Result result = GetResultForDayZero(FeatureConfig()); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.event_model_ready_ok); + EXPECT_FALSE(result.config_ok); +} + +TEST_F(FeatureConfigConditionValidatorTest, ReadyModelEmptyConfig) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo}, {}); + + EXPECT_TRUE(GetResultForDayZero(GetValidFeatureConfig()).NoErrors()); +} + +TEST_F(FeatureConfigConditionValidatorTest, ReadyModelAcceptingConfig) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo}, {}); + + EXPECT_TRUE(GetResultForDayZero(GetAcceptingFeatureConfig()).NoErrors()); +} + +TEST_F(FeatureConfigConditionValidatorTest, CurrentlyShowing) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo, kTestFeatureBar}, {}); + + validator_.NotifyIsShowing(kTestFeatureBar); + ConditionValidator::Result result = + GetResultForDayZero(GetAcceptingFeatureConfig()); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.currently_showing_ok); +} + +TEST_F(FeatureConfigConditionValidatorTest, Used) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo}, {}); + + FeatureConfig config = GetAcceptingFeatureConfig(); + config.used = EventConfig("used", Comparator(LESS_THAN, 0), 0, 0); + + ConditionValidator::Result result = GetResultForDayZero(config); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.used_ok); +} + +TEST_F(FeatureConfigConditionValidatorTest, Trigger) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo}, {}); + + FeatureConfig config = GetAcceptingFeatureConfig(); + config.trigger = EventConfig("trigger", Comparator(LESS_THAN, 0), 0, 0); + + ConditionValidator::Result result = GetResultForDayZero(config); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.trigger_ok); +} + +TEST_F(FeatureConfigConditionValidatorTest, SingleOKPrecondition) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo}, {}); + + FeatureConfig config = GetAcceptingFeatureConfig(); + config.event_configs.insert(EventConfig("event1", Comparator(ANY, 0), 0, 0)); + + EXPECT_TRUE(GetResultForDayZero(config).NoErrors()); +} + +TEST_F(FeatureConfigConditionValidatorTest, MultipleOKPreconditions) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo}, {}); + + FeatureConfig config = GetAcceptingFeatureConfig(); + config.event_configs.insert(EventConfig("event1", Comparator(ANY, 0), 0, 0)); + config.event_configs.insert(EventConfig("event2", Comparator(ANY, 0), 0, 0)); + + EXPECT_TRUE(GetResultForDayZero(config).NoErrors()); +} + +TEST_F(FeatureConfigConditionValidatorTest, OneOKThenOneFailingPrecondition) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo}, {}); + + FeatureConfig config = GetAcceptingFeatureConfig(); + config.event_configs.insert(EventConfig("event1", Comparator(ANY, 0), 0, 0)); + config.event_configs.insert( + EventConfig("event2", Comparator(LESS_THAN, 0), 0, 0)); + + ConditionValidator::Result result = GetResultForDayZero(config); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.preconditions_ok); +} + +TEST_F(FeatureConfigConditionValidatorTest, OneFailingThenOneOKPrecondition) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo}, {}); + + FeatureConfig config = GetAcceptingFeatureConfig(); + config.event_configs.insert(EventConfig("event1", Comparator(ANY, 0), 0, 0)); + config.event_configs.insert( + EventConfig("event2", Comparator(LESS_THAN, 0), 0, 0)); + + ConditionValidator::Result result = GetResultForDayZero(config); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.preconditions_ok); +} + +TEST_F(FeatureConfigConditionValidatorTest, TwoFailingPreconditions) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo}, {}); + + FeatureConfig config = GetAcceptingFeatureConfig(); + config.event_configs.insert( + EventConfig("event1", Comparator(LESS_THAN, 0), 0, 0)); + config.event_configs.insert( + EventConfig("event2", Comparator(LESS_THAN, 0), 0, 0)); + + ConditionValidator::Result result = GetResultForDayZero(config); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.preconditions_ok); +} + +TEST_F(FeatureConfigConditionValidatorTest, SessionRate) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo, kTestFeatureBar}, {}); + + FeatureConfig config = GetAcceptingFeatureConfig(); + config.session_rate = Comparator(LESS_THAN, 2u); + + EXPECT_TRUE(GetResultForDayZero(config).NoErrors()); + + validator_.NotifyIsShowing(kTestFeatureBar); + validator_.NotifyDismissed(kTestFeatureBar); + EXPECT_TRUE(GetResultForDayZero(config).NoErrors()); + + validator_.NotifyIsShowing(kTestFeatureBar); + validator_.NotifyDismissed(kTestFeatureBar); + ConditionValidator::Result result = GetResultForDayZero(config); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.session_rate_ok); + + validator_.NotifyIsShowing(kTestFeatureBar); + validator_.NotifyDismissed(kTestFeatureBar); + result = GetResultForDayZero(config); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.session_rate_ok); +} + +TEST_F(FeatureConfigConditionValidatorTest, Availability) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo, kTestFeatureBar}, {}); + + FeatureConfig config = GetAcceptingFeatureConfig(); + EXPECT_TRUE(GetResultForDayZero(config).NoErrors()); + EXPECT_TRUE(GetResultForDay(config, 100u).NoErrors()); + + // When the AvailabilityModel is not ready, it should fail. + availability_model_.SetIsReady(false); + ConditionValidator::Result result = GetResultForDayZero(config); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.availability_model_ready_ok); + result = GetResultForDay(config, 100u); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.availability_model_ready_ok); + + // Reset state back to ready. + availability_model_.SetIsReady(true); + + // For a feature that became available on day 2 that has to have been + // available for at least 1 day, it should start being accepted on day 3. + availability_model_.SetAvailability(&kTestFeatureFoo, 2u); + config.availability = Comparator(GREATER_THAN_OR_EQUAL, 1u); + result = GetResultForDay(config, 1u); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.availability_ok); + result = GetResultForDay(config, 2u); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.availability_ok); + EXPECT_TRUE(GetResultForDay(config, 3u).NoErrors()); + EXPECT_TRUE(GetResultForDay(config, 4u).NoErrors()); + + // For a feature that became available on day 10 that has to have been + // available for at least 3 days, it should start being accepted on day 13. + availability_model_.SetAvailability(&kTestFeatureFoo, 10u); + config.availability = Comparator(GREATER_THAN_OR_EQUAL, 3u); + result = GetResultForDay(config, 11u); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.availability_ok); + result = GetResultForDay(config, 12u); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.availability_ok); + EXPECT_TRUE(GetResultForDay(config, 13u).NoErrors()); + EXPECT_TRUE(GetResultForDay(config, 14u).NoErrors()); +} + +TEST_F(FeatureConfigConditionValidatorTest, SingleEventChangingComparator) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo}, {}); + + uint32_t current_day = 102u; + uint32_t window = 10u; + + // Create event with 10 events per day for three days. + Event event1; + event1.set_name("event1"); + test::SetEventCountForDay(&event1, 100u, 10u); + test::SetEventCountForDay(&event1, 101u, 10u); + test::SetEventCountForDay(&event1, 102u, 10u); + event_model_.SetEvent(event1); + + EXPECT_TRUE(GetResultForDayAndEventWindow(Comparator(LESS_THAN, 50u), window, + current_day) + .NoErrors()); + EXPECT_TRUE( + GetResultForDayAndEventWindow(Comparator(EQUAL, 30u), window, current_day) + .NoErrors()); + EXPECT_FALSE(GetResultForDayAndEventWindow(Comparator(LESS_THAN, 30u), window, + current_day) + .NoErrors()); +} + +TEST_F(FeatureConfigConditionValidatorTest, SingleEventChangingWindow) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo}, {}); + + Event event1; + event1.set_name("event1"); + test::SetEventCountForDay(&event1, 100u, 10u); + test::SetEventCountForDay(&event1, 101u, 10u); + test::SetEventCountForDay(&event1, 102u, 10u); + test::SetEventCountForDay(&event1, 103u, 10u); + test::SetEventCountForDay(&event1, 104u, 10u); + event_model_.SetEvent(event1); + + uint32_t current_day = 104u; + + EXPECT_FALSE(GetResultForDayAndEventWindow(Comparator(GREATER_THAN, 30u), 0, + current_day) + .NoErrors()); + EXPECT_FALSE(GetResultForDayAndEventWindow(Comparator(GREATER_THAN, 30u), 1u, + current_day) + .NoErrors()); + EXPECT_FALSE(GetResultForDayAndEventWindow(Comparator(GREATER_THAN, 30u), 2u, + current_day) + .NoErrors()); + EXPECT_FALSE(GetResultForDayAndEventWindow(Comparator(GREATER_THAN, 30u), 3u, + current_day) + .NoErrors()); + EXPECT_TRUE(GetResultForDayAndEventWindow(Comparator(GREATER_THAN, 30u), 4u, + current_day) + .NoErrors()); + EXPECT_TRUE(GetResultForDayAndEventWindow(Comparator(GREATER_THAN, 30u), 5u, + current_day) + .NoErrors()); +} + +TEST_F(FeatureConfigConditionValidatorTest, CapEarliestAcceptedDayAtEpoch) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo}, {}); + + Event event1; + event1.set_name("event1"); + test::SetEventCountForDay(&event1, 0, 10u); + test::SetEventCountForDay(&event1, 1u, 10u); + test::SetEventCountForDay(&event1, 2u, 10u); + event_model_.SetEvent(event1); + + uint32_t current_day = 100u; + + EXPECT_TRUE( + GetResultForDayAndEventWindow(Comparator(EQUAL, 10u), 99u, current_day) + .NoErrors()); + EXPECT_TRUE( + GetResultForDayAndEventWindow(Comparator(EQUAL, 20u), 100u, current_day) + .NoErrors()); + EXPECT_TRUE( + GetResultForDayAndEventWindow(Comparator(EQUAL, 30u), 101u, current_day) + .NoErrors()); + EXPECT_TRUE( + GetResultForDayAndEventWindow(Comparator(EQUAL, 30u), 1000u, current_day) + .NoErrors()); +} + +TEST_F(FeatureConfigConditionValidatorTest, TestMultipleEvents) { + base::test::ScopedFeatureList scoped_feature_list; + scoped_feature_list.InitWithFeatures({kTestFeatureFoo}, {}); + + Event event1; + event1.set_name("event1"); + test::SetEventCountForDay(&event1, 0, 10u); + test::SetEventCountForDay(&event1, 1u, 10u); + test::SetEventCountForDay(&event1, 2u, 10u); + event_model_.SetEvent(event1); + + Event event2; + event2.set_name("event2"); + test::SetEventCountForDay(&event2, 0, 5u); + test::SetEventCountForDay(&event2, 1u, 5u); + test::SetEventCountForDay(&event2, 2u, 5u); + event_model_.SetEvent(event2); + + uint32_t current_day = 100u; + + // Verify validator counts correctly for two events last 99 days. + FeatureConfig config = GetAcceptingFeatureConfig(); + config.event_configs.insert( + EventConfig("event1", Comparator(EQUAL, 10u), 99u, 0)); + config.event_configs.insert( + EventConfig("event2", Comparator(EQUAL, 5u), 99u, 0)); + ConditionValidator::Result result = validator_.MeetsConditions( + kTestFeatureFoo, config, event_model_, availability_model_, current_day); + EXPECT_TRUE(result.NoErrors()); + + // Verify validator counts correctly for two events last 100 days. + config = GetAcceptingFeatureConfig(); + config.event_configs.insert( + EventConfig("event1", Comparator(EQUAL, 20u), 100u, 0)); + config.event_configs.insert( + EventConfig("event2", Comparator(EQUAL, 10u), 100u, 0)); + result = validator_.MeetsConditions(kTestFeatureFoo, config, event_model_, + availability_model_, current_day); + EXPECT_TRUE(result.NoErrors()); + + // Verify validator counts correctly for two events last 101 days. + config = GetAcceptingFeatureConfig(); + config.event_configs.insert( + EventConfig("event1", Comparator(EQUAL, 30u), 101u, 0)); + config.event_configs.insert( + EventConfig("event2", Comparator(EQUAL, 15u), 101u, 0)); + result = validator_.MeetsConditions(kTestFeatureFoo, config, event_model_, + availability_model_, current_day); + EXPECT_TRUE(result.NoErrors()); + + // Verify validator counts correctly for two events last 101 days, and returns + // error when first event fails. + config = GetAcceptingFeatureConfig(); + config.event_configs.insert( + EventConfig("event1", Comparator(EQUAL, 0), 101u, 0)); + config.event_configs.insert( + EventConfig("event2", Comparator(EQUAL, 15u), 101u, 0)); + result = validator_.MeetsConditions(kTestFeatureFoo, config, event_model_, + availability_model_, current_day); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.preconditions_ok); + + // Verify validator counts correctly for two events last 101 days, and returns + // error when second event fails. + config = GetAcceptingFeatureConfig(); + config.event_configs.insert( + EventConfig("event1", Comparator(EQUAL, 30u), 101u, 0)); + config.event_configs.insert( + EventConfig("event2", Comparator(EQUAL, 0), 101u, 0)); + result = validator_.MeetsConditions(kTestFeatureFoo, config, event_model_, + availability_model_, current_day); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.preconditions_ok); + + // Verify validator counts correctly for two events last 101 days, and returns + // error when both events fail. + config = GetAcceptingFeatureConfig(); + config.event_configs.insert( + EventConfig("event1", Comparator(EQUAL, 0), 101u, 0)); + config.event_configs.insert( + EventConfig("event2", Comparator(EQUAL, 0), 101u, 0)); + result = validator_.MeetsConditions(kTestFeatureFoo, config, event_model_, + availability_model_, current_day); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.preconditions_ok); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/feature_config_event_storage_validator.cc b/chromium/components/feature_engagement/internal/feature_config_event_storage_validator.cc new file mode 100644 index 00000000000..588d6a83f00 --- /dev/null +++ b/chromium/components/feature_engagement/internal/feature_config_event_storage_validator.cc @@ -0,0 +1,90 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/feature_config_event_storage_validator.h" + +#include <unordered_map> +#include <unordered_set> + +#include "base/feature_list.h" +#include "components/feature_engagement/internal/configuration.h" +#include "components/feature_engagement/public/feature_list.h" + +namespace feature_engagement { + +FeatureConfigEventStorageValidator::FeatureConfigEventStorageValidator() = + default; + +FeatureConfigEventStorageValidator::~FeatureConfigEventStorageValidator() = + default; + +bool FeatureConfigEventStorageValidator::ShouldStore( + const std::string& event_name) const { + return should_store_event_names_.find(event_name) != + should_store_event_names_.end(); +} + +bool FeatureConfigEventStorageValidator::ShouldKeep( + const std::string& event_name, + uint32_t event_day, + uint32_t current_day) const { + // Should not keep events that will happen in the future. + if (event_day > current_day) + return false; + + // If no feature configuration mentioned the event, it should not be kept. + auto it = longest_storage_times_.find(event_name); + if (it == longest_storage_times_.end()) + return false; + + // Too old events should not be kept. + uint32_t longest_storage_time = it->second; + uint32_t age = current_day - event_day; + if (longest_storage_time <= age) + return false; + + return true; +} + +void FeatureConfigEventStorageValidator::InitializeFeatures( + FeatureVector features, + const Configuration& configuration) { + for (const auto* feature : features) { + if (!base::FeatureList::IsEnabled(*feature)) + continue; + + InitializeFeatureConfig(configuration.GetFeatureConfig(*feature)); + } +} + +void FeatureConfigEventStorageValidator::ClearForTesting() { + should_store_event_names_.clear(); + longest_storage_times_.clear(); +} + +void FeatureConfigEventStorageValidator::InitializeFeatureConfig( + const FeatureConfig& feature_config) { + InitializeEventConfig(feature_config.used); + InitializeEventConfig(feature_config.trigger); + + for (const auto& event_config : feature_config.event_configs) + InitializeEventConfig(event_config); +} + +void FeatureConfigEventStorageValidator::InitializeEventConfig( + const EventConfig& event_config) { + // Minimum storage time is 1 day. + if (event_config.storage < 1u) + return; + + // When minimum storage time is met, new events should always be stored. + should_store_event_names_.insert(event_config.name); + + // Track the longest time any configuration wants to store a particular event. + uint32_t current_longest_time = longest_storage_times_[event_config.name]; + if (event_config.storage > current_longest_time) + longest_storage_times_[event_config.name] = event_config.storage; +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/feature_config_event_storage_validator.h b/chromium/components/feature_engagement/internal/feature_config_event_storage_validator.h new file mode 100644 index 00000000000..b7dd0683a1f --- /dev/null +++ b/chromium/components/feature_engagement/internal/feature_config_event_storage_validator.h @@ -0,0 +1,63 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_FEATURE_CONFIG_EVENT_STORAGE_VALIDATOR_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_FEATURE_CONFIG_EVENT_STORAGE_VALIDATOR_H_ + +#include <string> +#include <unordered_map> +#include <unordered_set> + +#include "base/macros.h" +#include "components/feature_engagement/internal/event_storage_validator.h" +#include "components/feature_engagement/public/feature_list.h" + +namespace feature_engagement { +class Configuration; +struct EventConfig; +struct FeatureConfig; + +// A EventStorageValidator that uses the FeatureConfig as the source of truth. +class FeatureConfigEventStorageValidator : public EventStorageValidator { + public: + FeatureConfigEventStorageValidator(); + ~FeatureConfigEventStorageValidator() override; + + // EventStorageValidator implementation. + bool ShouldStore(const std::string& event_name) const override; + bool ShouldKeep(const std::string& event_name, + uint32_t event_day, + uint32_t current_day) const override; + + // Set up internal configuration required for the given |features|. + void InitializeFeatures(FeatureVector features, + const Configuration& configuration); + + // Resets the full state of this EventStorageValidator. After calling this + // method it is valid to call InitializeFeatures() again. + void ClearForTesting(); + + private: + // Updates the internal configuration with conditions from the given + // |feature_config|. + void InitializeFeatureConfig(const FeatureConfig& feature_config); + + // Updates the internal configuration with conditions from the given + // |event_config|. + void InitializeEventConfig(const EventConfig& event_config); + + // Contains an entry for each of the events that any EventConfig required to + // be stored. + std::unordered_set<std::string> should_store_event_names_; + + // Contains the longest time to store each event across all EventConfigs, + // as a number of days. + std::unordered_map<std::string, uint32_t> longest_storage_times_; + + DISALLOW_COPY_AND_ASSIGN(FeatureConfigEventStorageValidator); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_FEATURE_CONFIG_EVENT_STORAGE_VALIDATOR_H_ diff --git a/chromium/components/feature_engagement/internal/feature_config_event_storage_validator_unittest.cc b/chromium/components/feature_engagement/internal/feature_config_event_storage_validator_unittest.cc new file mode 100644 index 00000000000..6ec6afe5376 --- /dev/null +++ b/chromium/components/feature_engagement/internal/feature_config_event_storage_validator_unittest.cc @@ -0,0 +1,295 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/feature_config_event_storage_validator.h" + +#include <string> + +#include "base/feature_list.h" +#include "base/metrics/field_trial.h" +#include "base/test/scoped_feature_list.h" +#include "components/feature_engagement/internal/configuration.h" +#include "components/feature_engagement/internal/editable_configuration.h" +#include "components/feature_engagement/internal/event_model.h" +#include "components/feature_engagement/internal/proto/event.pb.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +namespace { + +const base::Feature kTestFeatureFoo{"test_foo", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureBar{"test_bar", + base::FEATURE_DISABLED_BY_DEFAULT}; + +FeatureConfig kNeverStored; +FeatureConfig kStoredInUsed1Day; +FeatureConfig kStoredInUsed2Days; +FeatureConfig kStoredInUsed10Days; +FeatureConfig kStoredInTrigger1Day; +FeatureConfig kStoredInTrigger2Days; +FeatureConfig kStoredInTrigger10Days; +FeatureConfig kStoredInEventConfigs1Day; +FeatureConfig kStoredInEventConfigs2Days; +FeatureConfig kStoredInEventConfigs10Days; + +void InitializeStorageFeatureConfigs() { + FeatureConfig default_config; + default_config.valid = true; + default_config.used = EventConfig("myevent", Comparator(ANY, 0), 0, 0); + default_config.trigger = EventConfig("myevent", Comparator(ANY, 0), 0, 0); + default_config.event_configs.insert( + EventConfig("myevent", Comparator(ANY, 0), 0, 0)); + default_config.event_configs.insert( + EventConfig("unrelated_event", Comparator(ANY, 0), 0, 100)); + default_config.session_rate = Comparator(ANY, 0); + default_config.availability = Comparator(ANY, 0); + + kNeverStored = default_config; + + kStoredInUsed1Day = default_config; + kStoredInUsed1Day.used = EventConfig("myevent", Comparator(ANY, 0), 0, 1); + + kStoredInUsed2Days = default_config; + kStoredInUsed2Days.used = EventConfig("myevent", Comparator(ANY, 0), 0, 2); + + kStoredInUsed10Days = default_config; + kStoredInUsed10Days.used = EventConfig("myevent", Comparator(ANY, 0), 0, 10); + + kStoredInTrigger1Day = default_config; + kStoredInTrigger1Day.trigger = + EventConfig("myevent", Comparator(ANY, 0), 0, 1); + + kStoredInTrigger2Days = default_config; + kStoredInTrigger2Days.trigger = + EventConfig("myevent", Comparator(ANY, 0), 0, 2); + + kStoredInTrigger10Days = default_config; + kStoredInTrigger10Days.trigger = + EventConfig("myevent", Comparator(ANY, 0), 0, 10); + + kStoredInEventConfigs1Day = default_config; + kStoredInEventConfigs1Day.event_configs.clear(); + kStoredInEventConfigs1Day.event_configs.insert( + EventConfig("myevent", Comparator(ANY, 0), 0, 0)); + kStoredInEventConfigs1Day.event_configs.insert( + EventConfig("myevent", Comparator(ANY, 0), 0, 1)); + kStoredInEventConfigs1Day.event_configs.insert( + EventConfig("unrelated_event", Comparator(ANY, 0), 0, 100)); + + kStoredInEventConfigs2Days = default_config; + kStoredInEventConfigs2Days.event_configs.clear(); + kStoredInEventConfigs2Days.event_configs.insert( + EventConfig("myevent", Comparator(ANY, 0), 0, 0)); + kStoredInEventConfigs2Days.event_configs.insert( + EventConfig("myevent", Comparator(ANY, 0), 0, 2)); + kStoredInEventConfigs2Days.event_configs.insert( + EventConfig("unrelated_event", Comparator(ANY, 0), 0, 100)); + + kStoredInEventConfigs10Days = default_config; + kStoredInEventConfigs10Days.event_configs.clear(); + kStoredInEventConfigs10Days.event_configs.insert( + EventConfig("myevent", Comparator(ANY, 0), 0, 0)); + kStoredInEventConfigs10Days.event_configs.insert( + EventConfig("myevent", Comparator(ANY, 0), 0, 10)); + kStoredInEventConfigs10Days.event_configs.insert( + EventConfig("unrelated_event", Comparator(ANY, 0), 0, 100)); +} + +class FeatureConfigEventStorageValidatorTest : public ::testing::Test { + public: + FeatureConfigEventStorageValidatorTest() : current_day_(100) { + InitializeStorageFeatureConfigs(); + } + + void UseConfig(const FeatureConfig& foo_config) { + FeatureVector features = {&kTestFeatureFoo}; + + validator_.ClearForTesting(); + EditableConfiguration configuration; + configuration.SetConfiguration(&kTestFeatureFoo, foo_config); + validator_.InitializeFeatures(features, configuration); + } + + void UseConfigs(const FeatureConfig& foo_config, + const FeatureConfig& bar_config) { + FeatureVector features = {&kTestFeatureFoo, &kTestFeatureBar}; + + validator_.ClearForTesting(); + EditableConfiguration configuration; + configuration.SetConfiguration(&kTestFeatureFoo, foo_config); + configuration.SetConfiguration(&kTestFeatureBar, bar_config); + validator_.InitializeFeatures(features, configuration); + } + + void VerifyNeverKeep() { + EXPECT_FALSE(validator_.ShouldKeep("myevent", 89, current_day_)); + EXPECT_FALSE(validator_.ShouldKeep("myevent", 90, current_day_)); + EXPECT_FALSE(validator_.ShouldKeep("myevent", 91, current_day_)); + EXPECT_FALSE(validator_.ShouldKeep("myevent", 98, current_day_)); + EXPECT_FALSE(validator_.ShouldKeep("myevent", 99, current_day_)); + EXPECT_FALSE(validator_.ShouldKeep("myevent", 100, current_day_)); + // This is trying to store data in the future, which should never happen. + EXPECT_FALSE(validator_.ShouldKeep("myevent", 101, current_day_)); + } + + void VerifyKeep1Day() { + EXPECT_FALSE(validator_.ShouldKeep("myevent", 89, current_day_)); + EXPECT_FALSE(validator_.ShouldKeep("myevent", 90, current_day_)); + EXPECT_FALSE(validator_.ShouldKeep("myevent", 91, current_day_)); + EXPECT_FALSE(validator_.ShouldKeep("myevent", 98, current_day_)); + EXPECT_FALSE(validator_.ShouldKeep("myevent", 99, current_day_)); + EXPECT_TRUE(validator_.ShouldKeep("myevent", 100, current_day_)); + // This is trying to store data in the future, which should never happen. + EXPECT_FALSE(validator_.ShouldKeep("myevent", 101, current_day_)); + } + + void VerifyKeep2Days() { + EXPECT_FALSE(validator_.ShouldKeep("myevent", 89, current_day_)); + EXPECT_FALSE(validator_.ShouldKeep("myevent", 90, current_day_)); + EXPECT_FALSE(validator_.ShouldKeep("myevent", 91, current_day_)); + EXPECT_FALSE(validator_.ShouldKeep("myevent", 98, current_day_)); + EXPECT_TRUE(validator_.ShouldKeep("myevent", 99, current_day_)); + EXPECT_TRUE(validator_.ShouldKeep("myevent", 100, current_day_)); + // This is trying to store data in the future, which should never happen. + EXPECT_FALSE(validator_.ShouldKeep("myevent", 101, current_day_)); + } + + void VerifyKeep10Days() { + EXPECT_FALSE(validator_.ShouldKeep("myevent", 89, current_day_)); + EXPECT_FALSE(validator_.ShouldKeep("myevent", 90, current_day_)); + EXPECT_TRUE(validator_.ShouldKeep("myevent", 91, current_day_)); + EXPECT_TRUE(validator_.ShouldKeep("myevent", 98, current_day_)); + EXPECT_TRUE(validator_.ShouldKeep("myevent", 99, current_day_)); + EXPECT_TRUE(validator_.ShouldKeep("myevent", 100, current_day_)); + // This is trying to store data in the future, which should never happen. + EXPECT_FALSE(validator_.ShouldKeep("myevent", 101, current_day_)); + } + + protected: + FeatureConfigEventStorageValidator validator_; + uint32_t current_day_; + base::test::ScopedFeatureList scoped_feature_list_; + + private: + DISALLOW_COPY_AND_ASSIGN(FeatureConfigEventStorageValidatorTest); +}; + +} // namespace + +TEST_F(FeatureConfigEventStorageValidatorTest, + ShouldOnlyUseConfigFromEnabledFeatures) { + scoped_feature_list_.InitWithFeatures({kTestFeatureFoo}, {kTestFeatureBar}); + + FeatureConfig foo_config = kNeverStored; + foo_config.used = EventConfig("fooevent", Comparator(ANY, 0), 0, 1); + FeatureConfig bar_config = kNeverStored; + bar_config.used = EventConfig("barevent", Comparator(ANY, 0), 0, 1); + UseConfigs(foo_config, bar_config); + + EXPECT_FALSE(validator_.ShouldStore("myevent")); + EXPECT_TRUE(validator_.ShouldStore("fooevent")); + EXPECT_FALSE(validator_.ShouldStore("barevent")); +} + +TEST_F(FeatureConfigEventStorageValidatorTest, + ShouldStoreIfSingleConfigHasMinimum1DayStorage) { + scoped_feature_list_.InitWithFeatures({kTestFeatureFoo}, {}); + + UseConfig(kNeverStored); + EXPECT_FALSE(validator_.ShouldStore("myevent")); + + const FeatureConfig* should_store_configs[] = { + &kStoredInUsed1Day, &kStoredInUsed2Days, + &kStoredInUsed10Days, &kStoredInTrigger1Day, + &kStoredInTrigger2Days, &kStoredInTrigger10Days, + &kStoredInEventConfigs1Day, &kStoredInEventConfigs2Days, + &kStoredInEventConfigs10Days}; + for (const FeatureConfig* config : should_store_configs) { + UseConfig(*config); + EXPECT_TRUE(validator_.ShouldStore("myevent")); + } +} + +TEST_F(FeatureConfigEventStorageValidatorTest, + ShouldStoreIfAnyConfigHasMinimum1DayStorage) { + scoped_feature_list_.InitWithFeatures({kTestFeatureFoo, kTestFeatureBar}, {}); + + UseConfigs(kNeverStored, kNeverStored); + EXPECT_FALSE(validator_.ShouldStore("myevent")); + + const FeatureConfig* should_store_configs[] = { + &kStoredInUsed1Day, &kStoredInUsed2Days, + &kStoredInUsed10Days, &kStoredInTrigger1Day, + &kStoredInTrigger2Days, &kStoredInTrigger10Days, + &kStoredInEventConfigs1Day, &kStoredInEventConfigs2Days, + &kStoredInEventConfigs10Days}; + for (const FeatureConfig* config : should_store_configs) { + UseConfigs(kNeverStored, *config); + EXPECT_TRUE(validator_.ShouldStore("myevent")); + } +} + +TEST_F(FeatureConfigEventStorageValidatorTest, + ShouldKeepIfSingleConfigMeetsEventAge) { + scoped_feature_list_.InitWithFeatures({kTestFeatureFoo}, {}); + + UseConfig(kNeverStored); + VerifyNeverKeep(); + + const FeatureConfig* one_day_storage_configs[] = { + &kStoredInUsed1Day, &kStoredInTrigger1Day, &kStoredInEventConfigs1Day}; + for (const FeatureConfig* config : one_day_storage_configs) { + UseConfig(*config); + VerifyKeep1Day(); + } + + const FeatureConfig* two_days_storage_configs[] = { + &kStoredInUsed2Days, &kStoredInTrigger2Days, &kStoredInEventConfigs2Days}; + for (const FeatureConfig* config : two_days_storage_configs) { + UseConfig(*config); + VerifyKeep2Days(); + } + + const FeatureConfig* ten_days_storage_configs[] = { + &kStoredInUsed10Days, &kStoredInTrigger10Days, + &kStoredInEventConfigs10Days}; + for (const FeatureConfig* config : ten_days_storage_configs) { + UseConfig(*config); + VerifyKeep10Days(); + } +} + +TEST_F(FeatureConfigEventStorageValidatorTest, + ShouldKeepIfAnyConfigMeetsEventAge) { + scoped_feature_list_.InitWithFeatures({kTestFeatureFoo, kTestFeatureBar}, {}); + + UseConfigs(kNeverStored, kNeverStored); + VerifyNeverKeep(); + + const FeatureConfig* one_day_storage_configs[] = { + &kStoredInUsed1Day, &kStoredInTrigger1Day, &kStoredInEventConfigs1Day}; + for (const FeatureConfig* config : one_day_storage_configs) { + UseConfigs(kNeverStored, *config); + VerifyKeep1Day(); + } + + const FeatureConfig* two_days_storage_configs[] = { + &kStoredInUsed2Days, &kStoredInTrigger2Days, &kStoredInEventConfigs2Days}; + for (const FeatureConfig* config : two_days_storage_configs) { + UseConfigs(kNeverStored, *config); + VerifyKeep2Days(); + } + + const FeatureConfig* ten_days_storage_configs[] = { + &kStoredInUsed10Days, &kStoredInTrigger10Days, + &kStoredInEventConfigs10Days}; + for (const FeatureConfig* config : ten_days_storage_configs) { + UseConfigs(kNeverStored, *config); + VerifyKeep10Days(); + } +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/in_memory_event_store.cc b/chromium/components/feature_engagement/internal/in_memory_event_store.cc new file mode 100644 index 00000000000..a19b41076fc --- /dev/null +++ b/chromium/components/feature_engagement/internal/in_memory_event_store.cc @@ -0,0 +1,51 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/in_memory_event_store.h" + +#include <vector> + +#include "base/bind.h" +#include "base/feature_list.h" +#include "base/memory/ptr_util.h" +#include "base/sequenced_task_runner.h" +#include "base/single_thread_task_runner.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/feature_engagement/internal/event_store.h" + +namespace feature_engagement { + +InMemoryEventStore::InMemoryEventStore( + std::unique_ptr<std::vector<Event>> events) + : EventStore(), events_(std::move(events)), ready_(false) {} + +InMemoryEventStore::InMemoryEventStore() + : InMemoryEventStore(base::MakeUnique<std::vector<Event>>()) {} + +InMemoryEventStore::~InMemoryEventStore() = default; + +void InMemoryEventStore::Load(const OnLoadedCallback& callback) { + HandleLoadResult(callback, true); +} + +bool InMemoryEventStore::IsReady() const { + return ready_; +} + +void InMemoryEventStore::WriteEvent(const Event& event) { + // Intentionally ignore all writes. +} + +void InMemoryEventStore::DeleteEvent(const std::string& event_name) { + // Intentionally ignore all deletes. +} + +void InMemoryEventStore::HandleLoadResult(const OnLoadedCallback& callback, + bool success) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, success, base::Passed(&events_))); + ready_ = success; +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/in_memory_event_store.h b/chromium/components/feature_engagement/internal/in_memory_event_store.h new file mode 100644 index 00000000000..cf6da5016fc --- /dev/null +++ b/chromium/components/feature_engagement/internal/in_memory_event_store.h @@ -0,0 +1,48 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_IN_MEMORY_EVENT_STORE_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_IN_MEMORY_EVENT_STORE_H_ + +#include <vector> + +#include "base/macros.h" +#include "components/feature_engagement/internal/event_store.h" + +namespace feature_engagement { +// An InMemoryEventStore provides a DB layer that stores all data in-memory. +// All data is made available to this class during construction, and can be +// loaded once by a caller. All calls to WriteEvent(...) are ignored. +class InMemoryEventStore : public EventStore { + public: + explicit InMemoryEventStore(std::unique_ptr<std::vector<Event>> events); + InMemoryEventStore(); + ~InMemoryEventStore() override; + + // EventStore implementation. + void Load(const OnLoadedCallback& callback) override; + bool IsReady() const override; + void WriteEvent(const Event& event) override; + void DeleteEvent(const std::string& event_name) override; + + protected: + // Posts the result of loading and sets up the ready state. + // Protected and virtual for testing. + virtual void HandleLoadResult(const OnLoadedCallback& callback, bool success); + + private: + // All events that this in-memory store was constructed with. This will be + // reset when Load(...) is called. + std::unique_ptr<std::vector<Event>> events_; + + // Whether the store is ready or not. It is true after Load(...) has been + // invoked. + bool ready_; + + DISALLOW_COPY_AND_ASSIGN(InMemoryEventStore); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_IN_MEMORY_EVENT_STORE_H_ diff --git a/chromium/components/feature_engagement/internal/in_memory_event_store_unittest.cc b/chromium/components/feature_engagement/internal/in_memory_event_store_unittest.cc new file mode 100644 index 00000000000..e3c48510b30 --- /dev/null +++ b/chromium/components/feature_engagement/internal/in_memory_event_store_unittest.cc @@ -0,0 +1,70 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/in_memory_event_store.h" + +#include <memory> +#include <utility> +#include <vector> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/memory/ptr_util.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +namespace { + +class InMemoryEventStoreTest : public ::testing::Test { + public: + InMemoryEventStoreTest() + : load_callback_has_been_invoked_(false), last_result_(false) {} + + void LoadCallback(bool success, std::unique_ptr<std::vector<Event>> events) { + load_callback_has_been_invoked_ = true; + last_result_ = success; + loaded_events_ = std::move(events); + } + + protected: + bool load_callback_has_been_invoked_; + bool last_result_; + std::unique_ptr<std::vector<Event>> loaded_events_; + base::MessageLoop message_loop_; +}; +} // namespace + +TEST_F(InMemoryEventStoreTest, LoadShouldProvideEventsAsCallback) { + std::unique_ptr<std::vector<Event>> events = + base::MakeUnique<std::vector<Event>>(); + Event foo; + Event bar; + events->push_back(foo); + events->push_back(bar); + + // Create a new store and verify it's not ready yet. + InMemoryEventStore store(std::move(events)); + EXPECT_FALSE(store.IsReady()); + + // Load the data and ensure the callback is not immediately invoked, since the + // result should be posted. + store.Load(base::Bind(&InMemoryEventStoreTest::LoadCallback, + base::Unretained(this))); + EXPECT_FALSE(load_callback_has_been_invoked_); + + // Run the message loop until it's idle to finish to ensure the result is + // available. + base::RunLoop().RunUntilIdle(); + + // The two events should have been loaded, and the store should be ready. + EXPECT_TRUE(load_callback_has_been_invoked_); + EXPECT_TRUE(store.IsReady()); + EXPECT_EQ(2u, loaded_events_->size()); + EXPECT_TRUE(last_result_); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/init_aware_event_model.cc b/chromium/components/feature_engagement/internal/init_aware_event_model.cc new file mode 100644 index 00000000000..9d53ab94bcc --- /dev/null +++ b/chromium/components/feature_engagement/internal/init_aware_event_model.cc @@ -0,0 +1,69 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/init_aware_event_model.h" + +#include "base/bind.h" + +namespace feature_engagement { + +InitAwareEventModel::InitAwareEventModel( + std::unique_ptr<EventModel> event_model) + : event_model_(std::move(event_model)), + initialization_complete_(false), + weak_ptr_factory_(this) { + DCHECK(event_model_); +} + +InitAwareEventModel::~InitAwareEventModel() = default; + +void InitAwareEventModel::Initialize( + const OnModelInitializationFinished& callback, + uint32_t current_day) { + event_model_->Initialize( + base::Bind(&InitAwareEventModel::OnInitializeComplete, + weak_ptr_factory_.GetWeakPtr(), callback), + current_day); +} + +bool InitAwareEventModel::IsReady() const { + return event_model_->IsReady(); +} + +const Event* InitAwareEventModel::GetEvent( + const std::string& event_name) const { + return event_model_->GetEvent(event_name); +} + +void InitAwareEventModel::IncrementEvent(const std::string& event_name, + uint32_t current_day) { + if (IsReady()) { + event_model_->IncrementEvent(event_name, current_day); + return; + } + + if (initialization_complete_) + return; + + queued_events_.push_back(std::tie(event_name, current_day)); +} + +void InitAwareEventModel::OnInitializeComplete( + const OnModelInitializationFinished& callback, + bool success) { + initialization_complete_ = true; + if (success) { + for (auto& event : queued_events_) + event_model_->IncrementEvent(std::get<0>(event), std::get<1>(event)); + } + queued_events_.clear(); + + callback.Run(success); +} + +size_t InitAwareEventModel::GetQueuedEventCountForTesting() { + return queued_events_.size(); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/init_aware_event_model.h b/chromium/components/feature_engagement/internal/init_aware_event_model.h new file mode 100644 index 00000000000..ed034b7cca4 --- /dev/null +++ b/chromium/components/feature_engagement/internal/init_aware_event_model.h @@ -0,0 +1,54 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_INIT_AWARE_EVENT_MODEL_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_INIT_AWARE_EVENT_MODEL_H_ + +#include <stdint.h> + +#include <memory> +#include <string> +#include <tuple> +#include <vector> + +#include "base/memory/weak_ptr.h" +#include "components/feature_engagement/internal/event_model.h" + +namespace feature_engagement { + +class InitAwareEventModel : public EventModel { + public: + InitAwareEventModel(std::unique_ptr<EventModel> event_model); + ~InitAwareEventModel() override; + + // EventModel implementation. + void Initialize(const OnModelInitializationFinished& callback, + uint32_t current_day) override; + bool IsReady() const override; + const Event* GetEvent(const std::string& event_name) const override; + void IncrementEvent(const std::string& event_name, + uint32_t current_day) override; + + size_t GetQueuedEventCountForTesting(); + + private: + void OnInitializeComplete(const OnModelInitializationFinished& callback, + bool success); + + std::unique_ptr<EventModel> event_model_; + std::vector<std::tuple<std::string, uint32_t>> queued_events_; + + // Whether the initialization has completed. This will be set to true once + // the underlying event model has been initialized, regardless of whether the + // result was a success or not. + bool initialization_complete_; + + base::WeakPtrFactory<InitAwareEventModel> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(InitAwareEventModel); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_INIT_AWARE_EVENT_MODEL_H_ diff --git a/chromium/components/feature_engagement/internal/init_aware_event_model_unittest.cc b/chromium/components/feature_engagement/internal/init_aware_event_model_unittest.cc new file mode 100644 index 00000000000..b3129588b8f --- /dev/null +++ b/chromium/components/feature_engagement/internal/init_aware_event_model_unittest.cc @@ -0,0 +1,170 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/init_aware_event_model.h" + +#include <memory> + +#include "base/bind.h" +#include "base/macros.h" +#include "base/memory/ptr_util.h" +#include "base/optional.h" +#include "components/feature_engagement/internal/proto/event.pb.h" +#include "components/feature_engagement/internal/test/event_util.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; +using testing::Return; +using testing::SaveArg; +using testing::Sequence; + +namespace feature_engagement { + +namespace { + +class MockEventModel : public EventModel { + public: + MockEventModel() = default; + ~MockEventModel() override = default; + + // EventModel implementation. + MOCK_METHOD2(Initialize, + void(const OnModelInitializationFinished&, uint32_t)); + MOCK_CONST_METHOD0(IsReady, bool()); + MOCK_CONST_METHOD1(GetEvent, Event*(const std::string&)); + MOCK_METHOD2(IncrementEvent, void(const std::string&, uint32_t)); + + private: + DISALLOW_COPY_AND_ASSIGN(MockEventModel); +}; + +class InitAwareEventModelTest : public testing::Test { + public: + InitAwareEventModelTest() : mocked_model_(nullptr) { + load_callback_ = base::Bind(&InitAwareEventModelTest::OnModelInitialized, + base::Unretained(this)); + } + + ~InitAwareEventModelTest() override = default; + + void SetUp() override { + auto mocked_model = base::MakeUnique<MockEventModel>(); + mocked_model_ = mocked_model.get(); + model_ = base::MakeUnique<InitAwareEventModel>(std::move(mocked_model)); + } + + protected: + void OnModelInitialized(bool success) { load_success_ = success; } + + std::unique_ptr<InitAwareEventModel> model_; + MockEventModel* mocked_model_; + + // Load callback tracking. + base::Optional<bool> load_success_; + EventModel::OnModelInitializationFinished load_callback_; + + private: + DISALLOW_COPY_AND_ASSIGN(InitAwareEventModelTest); +}; + +} // namespace + +TEST_F(InitAwareEventModelTest, PassThroughIsReady) { + EXPECT_CALL(*mocked_model_, IsReady()).Times(1); + model_->IsReady(); +} + +TEST_F(InitAwareEventModelTest, PassThroughGetEvent) { + Event foo; + foo.set_name("foo"); + test::SetEventCountForDay(&foo, 1, 1); + + EXPECT_CALL(*mocked_model_, GetEvent(foo.name())) + .WillRepeatedly(Return(&foo)); + EXPECT_CALL(*mocked_model_, GetEvent("bar")).WillRepeatedly(Return(nullptr)); + + test::VerifyEventsEqual(&foo, model_->GetEvent(foo.name())); + EXPECT_EQ(nullptr, model_->GetEvent("bar")); +} + +TEST_F(InitAwareEventModelTest, PassThroughIncrementEvent) { + EXPECT_CALL(*mocked_model_, IsReady()).WillRepeatedly(Return(true)); + + Sequence sequence; + EXPECT_CALL(*mocked_model_, IncrementEvent("foo", 0U)).InSequence(sequence); + EXPECT_CALL(*mocked_model_, IncrementEvent("bar", 1U)).InSequence(sequence); + + model_->IncrementEvent("foo", 0U); + model_->IncrementEvent("bar", 1U); + EXPECT_EQ(0U, model_->GetQueuedEventCountForTesting()); +} + +TEST_F(InitAwareEventModelTest, QueuedIncrementEvent) { + { + EXPECT_CALL(*mocked_model_, IsReady()).WillRepeatedly(Return(false)); + EXPECT_CALL(*mocked_model_, IncrementEvent(_, _)).Times(0); + + model_->IncrementEvent("foo", 0U); + model_->IncrementEvent("bar", 1U); + } + + EventModel::OnModelInitializationFinished callback; + EXPECT_CALL(*mocked_model_, Initialize(_, 2U)) + .WillOnce(SaveArg<0>(&callback)); + model_->Initialize(load_callback_, 2U); + + { + Sequence sequence; + EXPECT_CALL(*mocked_model_, IncrementEvent("foo", 0U)) + .Times(1) + .InSequence(sequence); + EXPECT_CALL(*mocked_model_, IncrementEvent("bar", 1U)) + .Times(1) + .InSequence(sequence); + + callback.Run(true); + EXPECT_TRUE(load_success_.value()); + } + + EXPECT_CALL(*mocked_model_, IsReady()).WillRepeatedly(Return(true)); + EXPECT_CALL(*mocked_model_, IncrementEvent("qux", 3U)).Times(1); + model_->IncrementEvent("qux", 3U); + EXPECT_EQ(0U, model_->GetQueuedEventCountForTesting()); +} + +TEST_F(InitAwareEventModelTest, QueuedIncrementEventWithUnsuccessfulInit) { + { + EXPECT_CALL(*mocked_model_, IsReady()).WillRepeatedly(Return(false)); + EXPECT_CALL(*mocked_model_, IncrementEvent(_, _)).Times(0); + + model_->IncrementEvent("foo", 0U); + model_->IncrementEvent("bar", 1U); + } + + EventModel::OnModelInitializationFinished callback; + EXPECT_CALL(*mocked_model_, Initialize(_, 2U)) + .WillOnce(SaveArg<0>(&callback)); + model_->Initialize(load_callback_, 2U); + + { + Sequence sequence; + EXPECT_CALL(*mocked_model_, IncrementEvent("foo", 0U)) + .Times(0) + .InSequence(sequence); + EXPECT_CALL(*mocked_model_, IncrementEvent("bar", 1U)) + .Times(0) + .InSequence(sequence); + + callback.Run(false); + EXPECT_FALSE(load_success_.value()); + EXPECT_EQ(0U, model_->GetQueuedEventCountForTesting()); + } + + EXPECT_CALL(*mocked_model_, IncrementEvent("qux", 3U)).Times(0); + model_->IncrementEvent("qux", 3U); + EXPECT_EQ(0U, model_->GetQueuedEventCountForTesting()); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/never_availability_model.cc b/chromium/components/feature_engagement/internal/never_availability_model.cc new file mode 100644 index 00000000000..1b4615d8382 --- /dev/null +++ b/chromium/components/feature_engagement/internal/never_availability_model.cc @@ -0,0 +1,45 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/never_availability_model.h" + +#include <utility> + +#include "base/callback.h" +#include "base/optional.h" +#include "base/sequenced_task_runner.h" +#include "base/single_thread_task_runner.h" +#include "base/threading/thread_task_runner_handle.h" + +namespace feature_engagement { + +NeverAvailabilityModel::NeverAvailabilityModel() : ready_(false) {} + +NeverAvailabilityModel::~NeverAvailabilityModel() = default; + +void NeverAvailabilityModel::Initialize(OnInitializedCallback callback, + uint32_t current_day) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(&NeverAvailabilityModel::ForwardedOnInitializedCallback, + base::Unretained(this), std::move(callback))); +} + +bool NeverAvailabilityModel::IsReady() const { + return ready_; +} + +base::Optional<uint32_t> NeverAvailabilityModel::GetAvailability( + const base::Feature& feature) const { + return base::nullopt; +} + +void NeverAvailabilityModel::ForwardedOnInitializedCallback( + OnInitializedCallback callback) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), true)); + ready_ = true; +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/never_availability_model.h b/chromium/components/feature_engagement/internal/never_availability_model.h new file mode 100644 index 00000000000..9d30f1a83ad --- /dev/null +++ b/chromium/components/feature_engagement/internal/never_availability_model.h @@ -0,0 +1,43 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_NEVER_AVAILABILITY_MODEL_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_NEVER_AVAILABILITY_MODEL_H_ + +#include <stdint.h> + +#include "base/macros.h" +#include "components/feature_engagement/internal/availability_model.h" + +namespace feature_engagement { + +// An AvailabilityModel that never has any data, and is ready after having been +// initialized. +class NeverAvailabilityModel : public AvailabilityModel { + public: + NeverAvailabilityModel(); + ~NeverAvailabilityModel() override; + + // AvailabilityModel implementation. + void Initialize(AvailabilityModel::OnInitializedCallback callback, + uint32_t current_day) override; + bool IsReady() const override; + base::Optional<uint32_t> GetAvailability( + const base::Feature& feature) const override; + + private: + // Sets |ready_| to true and posts the result to |callback|. This method + // exists to ensure that |ready_| is not updated directly in the + // Initialize(...) method. + void ForwardedOnInitializedCallback(OnInitializedCallback callback); + + // Whether the model has been successfully initialized. + bool ready_; + + DISALLOW_COPY_AND_ASSIGN(NeverAvailabilityModel); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_NEVER_AVAILABILITY_MODEL_H_ diff --git a/chromium/components/feature_engagement/internal/never_availability_model_unittest.cc b/chromium/components/feature_engagement/internal/never_availability_model_unittest.cc new file mode 100644 index 00000000000..099df237f2e --- /dev/null +++ b/chromium/components/feature_engagement/internal/never_availability_model_unittest.cc @@ -0,0 +1,73 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/never_availability_model.h" + +#include "base/bind.h" +#include "base/feature_list.h" +#include "base/message_loop/message_loop.h" +#include "base/optional.h" +#include "base/run_loop.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +namespace { + +const base::Feature kTestFeatureFoo{"test_foo", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureBar{"test_bar", + base::FEATURE_DISABLED_BY_DEFAULT}; + +class NeverAvailabilityModelTest : public ::testing::Test { + public: + NeverAvailabilityModelTest() = default; + + void OnInitializedCallback(bool success) { success_ = success; } + + protected: + NeverAvailabilityModel availability_model_; + base::Optional<bool> success_; + + private: + base::MessageLoop message_loop_; + + DISALLOW_COPY_AND_ASSIGN(NeverAvailabilityModelTest); +}; + +} // namespace + +TEST_F(NeverAvailabilityModelTest, ShouldNeverHaveData) { + EXPECT_EQ(base::nullopt, + availability_model_.GetAvailability(kTestFeatureFoo)); + EXPECT_EQ(base::nullopt, + availability_model_.GetAvailability(kTestFeatureBar)); + + availability_model_.Initialize( + base::BindOnce(&NeverAvailabilityModelTest::OnInitializedCallback, + base::Unretained(this)), + 14u); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(base::nullopt, + availability_model_.GetAvailability(kTestFeatureFoo)); + EXPECT_EQ(base::nullopt, + availability_model_.GetAvailability(kTestFeatureBar)); +} + +TEST_F(NeverAvailabilityModelTest, ShouldBeReadyAfterInitialization) { + EXPECT_FALSE(availability_model_.IsReady()); + availability_model_.Initialize( + base::BindOnce(&NeverAvailabilityModelTest::OnInitializedCallback, + base::Unretained(this)), + 14u); + EXPECT_FALSE(availability_model_.IsReady()); + EXPECT_FALSE(success_.has_value()); + base::RunLoop().RunUntilIdle(); + EXPECT_TRUE(availability_model_.IsReady()); + ASSERT_TRUE(success_.has_value()); + EXPECT_TRUE(success_.value()); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/never_condition_validator.cc b/chromium/components/feature_engagement/internal/never_condition_validator.cc new file mode 100644 index 00000000000..038eedf610e --- /dev/null +++ b/chromium/components/feature_engagement/internal/never_condition_validator.cc @@ -0,0 +1,26 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/never_condition_validator.h" + +namespace feature_engagement { + +NeverConditionValidator::NeverConditionValidator() = default; + +NeverConditionValidator::~NeverConditionValidator() = default; + +ConditionValidator::Result NeverConditionValidator::MeetsConditions( + const base::Feature& feature, + const FeatureConfig& config, + const EventModel& event_model, + const AvailabilityModel& availability_model, + uint32_t current_day) const { + return ConditionValidator::Result(false); +} + +void NeverConditionValidator::NotifyIsShowing(const base::Feature& feature) {} + +void NeverConditionValidator::NotifyDismissed(const base::Feature& feature) {} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/never_condition_validator.h b/chromium/components/feature_engagement/internal/never_condition_validator.h new file mode 100644 index 00000000000..b915f838096 --- /dev/null +++ b/chromium/components/feature_engagement/internal/never_condition_validator.h @@ -0,0 +1,43 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_NEVER_CONDITION_VALIDATOR_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_NEVER_CONDITION_VALIDATOR_H_ + +#include "base/macros.h" +#include "components/feature_engagement/internal/condition_validator.h" +#include "components/feature_engagement/public/feature_list.h" + +namespace base { +struct Feature; +} // namespace base + +namespace feature_engagement { +class AvailabilityModel; +class EventModel; + +// An ConditionValidator that never acknowledges that a feature has met its +// conditions. +class NeverConditionValidator : public ConditionValidator { + public: + NeverConditionValidator(); + ~NeverConditionValidator() override; + + // ConditionValidator implementation. + ConditionValidator::Result MeetsConditions( + const base::Feature& feature, + const FeatureConfig& config, + const EventModel& event_model, + const AvailabilityModel& availability_model, + uint32_t current_day) const override; + void NotifyIsShowing(const base::Feature& feature) override; + void NotifyDismissed(const base::Feature& feature) override; + + private: + DISALLOW_COPY_AND_ASSIGN(NeverConditionValidator); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_NEVER_CONDITION_VALIDATOR_H_ diff --git a/chromium/components/feature_engagement/internal/never_condition_validator_unittest.cc b/chromium/components/feature_engagement/internal/never_condition_validator_unittest.cc new file mode 100644 index 00000000000..bcf1b52cb80 --- /dev/null +++ b/chromium/components/feature_engagement/internal/never_condition_validator_unittest.cc @@ -0,0 +1,71 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/never_condition_validator.h" + +#include <string> + +#include "base/feature_list.h" +#include "components/feature_engagement/internal/configuration.h" +#include "components/feature_engagement/internal/event_model.h" +#include "components/feature_engagement/internal/never_availability_model.h" +#include "components/feature_engagement/internal/proto/event.pb.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +namespace { + +const base::Feature kTestFeatureFoo{"test_foo", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureBar{"test_bar", + base::FEATURE_DISABLED_BY_DEFAULT}; + +// A EventModel that is always postive to show in-product help. +class TestEventModel : public EventModel { + public: + TestEventModel() = default; + + void Initialize(const OnModelInitializationFinished& callback, + uint32_t current_day) override {} + + bool IsReady() const override { return true; } + + const Event* GetEvent(const std::string& event_name) const override { + return nullptr; + } + + void IncrementEvent(const std::string& event_name, uint32_t day) override {} + + private: + DISALLOW_COPY_AND_ASSIGN(TestEventModel); +}; + +class NeverConditionValidatorTest : public ::testing::Test { + public: + NeverConditionValidatorTest() = default; + + protected: + TestEventModel event_model_; + NeverAvailabilityModel availability_model_; + NeverConditionValidator validator_; + + private: + DISALLOW_COPY_AND_ASSIGN(NeverConditionValidatorTest); +}; + +} // namespace + +TEST_F(NeverConditionValidatorTest, ShouldNeverMeetConditions) { + EXPECT_FALSE(validator_ + .MeetsConditions(kTestFeatureFoo, FeatureConfig(), + event_model_, availability_model_, 0u) + .NoErrors()); + EXPECT_FALSE(validator_ + .MeetsConditions(kTestFeatureBar, FeatureConfig(), + event_model_, availability_model_, 0u) + .NoErrors()); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/never_event_storage_validator.cc b/chromium/components/feature_engagement/internal/never_event_storage_validator.cc new file mode 100644 index 00000000000..521cb5532c9 --- /dev/null +++ b/chromium/components/feature_engagement/internal/never_event_storage_validator.cc @@ -0,0 +1,24 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/never_event_storage_validator.h" + +namespace feature_engagement { + +NeverEventStorageValidator::NeverEventStorageValidator() = default; + +NeverEventStorageValidator::~NeverEventStorageValidator() = default; + +bool NeverEventStorageValidator::ShouldStore( + const std::string& event_name) const { + return false; +} + +bool NeverEventStorageValidator::ShouldKeep(const std::string& event_name, + uint32_t event_day, + uint32_t current_day) const { + return false; +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/never_event_storage_validator.h b/chromium/components/feature_engagement/internal/never_event_storage_validator.h new file mode 100644 index 00000000000..10ed6798176 --- /dev/null +++ b/chromium/components/feature_engagement/internal/never_event_storage_validator.h @@ -0,0 +1,34 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_NEVER_EVENT_STORAGE_VALIDATOR_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_NEVER_EVENT_STORAGE_VALIDATOR_H_ + +#include <string> + +#include "base/macros.h" +#include "components/feature_engagement/internal/event_storage_validator.h" + +namespace feature_engagement { + +// A EventStorageValidator that never acknowledges that an event should be kept +// or stored. +class NeverEventStorageValidator : public EventStorageValidator { + public: + NeverEventStorageValidator(); + ~NeverEventStorageValidator() override; + + // EventStorageValidator implementation. + bool ShouldStore(const std::string& event_name) const override; + bool ShouldKeep(const std::string& event_name, + uint32_t event_day, + uint32_t current_day) const override; + + private: + DISALLOW_COPY_AND_ASSIGN(NeverEventStorageValidator); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_NEVER_EVENT_STORAGE_VALIDATOR_H_ diff --git a/chromium/components/feature_engagement/internal/never_event_storage_validator_unittest.cc b/chromium/components/feature_engagement/internal/never_event_storage_validator_unittest.cc new file mode 100644 index 00000000000..42157625e76 --- /dev/null +++ b/chromium/components/feature_engagement/internal/never_event_storage_validator_unittest.cc @@ -0,0 +1,36 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/never_event_storage_validator.h" + +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +namespace { + +class NeverEventStorageValidatorTest : public ::testing::Test { + public: + NeverEventStorageValidatorTest() = default; + + protected: + NeverEventStorageValidator validator_; + + private: + DISALLOW_COPY_AND_ASSIGN(NeverEventStorageValidatorTest); +}; + +} // namespace + +TEST_F(NeverEventStorageValidatorTest, ShouldNeverKeep) { + EXPECT_FALSE(validator_.ShouldStore("dummy event")); +} + +TEST_F(NeverEventStorageValidatorTest, ShouldNeverStore) { + EXPECT_FALSE(validator_.ShouldKeep("dummy event", 99, 100)); + EXPECT_FALSE(validator_.ShouldKeep("dummy event", 100, 100)); + EXPECT_FALSE(validator_.ShouldKeep("dummy event", 101, 100)); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/once_condition_validator.cc b/chromium/components/feature_engagement/internal/once_condition_validator.cc new file mode 100644 index 00000000000..f3068f318e3 --- /dev/null +++ b/chromium/components/feature_engagement/internal/once_condition_validator.cc @@ -0,0 +1,49 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/once_condition_validator.h" + +#include "components/feature_engagement/internal/configuration.h" +#include "components/feature_engagement/internal/event_model.h" + +namespace feature_engagement { + +OnceConditionValidator::OnceConditionValidator() = default; + +OnceConditionValidator::~OnceConditionValidator() = default; + +ConditionValidator::Result OnceConditionValidator::MeetsConditions( + const base::Feature& feature, + const FeatureConfig& config, + const EventModel& event_model, + const AvailabilityModel& availability_model, + uint32_t current_day) const { + ConditionValidator::Result result(true); + result.event_model_ready_ok = event_model.IsReady(); + + result.currently_showing_ok = currently_showing_feature_.empty(); + + result.config_ok = config.valid; + + result.trigger_ok = + shown_features_.find(feature.name) == shown_features_.end(); + result.session_rate_ok = + shown_features_.find(feature.name) == shown_features_.end(); + + return result; +} + +void OnceConditionValidator::NotifyIsShowing(const base::Feature& feature) { + DCHECK(currently_showing_feature_.empty()); + DCHECK(shown_features_.find(feature.name) == shown_features_.end()); + shown_features_.insert(feature.name); + currently_showing_feature_ = feature.name; +} + +void OnceConditionValidator::NotifyDismissed(const base::Feature& feature) { + DCHECK(feature.name == currently_showing_feature_); + currently_showing_feature_.clear(); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/once_condition_validator.h b/chromium/components/feature_engagement/internal/once_condition_validator.h new file mode 100644 index 00000000000..0bafc34e13c --- /dev/null +++ b/chromium/components/feature_engagement/internal/once_condition_validator.h @@ -0,0 +1,63 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_ONCE_CONDITION_VALIDATOR_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_ONCE_CONDITION_VALIDATOR_H_ + +#include <unordered_set> + +#include "base/macros.h" +#include "components/feature_engagement/internal/condition_validator.h" +#include "components/feature_engagement/public/feature_list.h" + +namespace base { +struct Feature; +} // namespace base + +namespace feature_engagement { +class AvailabilityModel; +class EventModel; + +// An ConditionValidator that will ensure that each base::Feature will meet +// conditions maximum one time for any given session. +// It has the following requirements: +// - The EventModel is ready. +// - No other in-product help is currently showing. +// - FeatureConfig for the feature is valid. +// - This is the first time the given base::Feature meets all above stated +// conditions. +// +// NOTE: This ConditionValidator fully ignores whether the base::Feature is +// enabled or not and any other configuration specified in the FeatureConfig. +// In practice this leads this ConditionValidator to be well suited for a +// demonstration mode of in-product help. +class OnceConditionValidator : public ConditionValidator { + public: + OnceConditionValidator(); + ~OnceConditionValidator() override; + + // ConditionValidator implementation. + ConditionValidator::Result MeetsConditions( + const base::Feature& feature, + const FeatureConfig& config, + const EventModel& event_model, + const AvailabilityModel& availability_model, + uint32_t current_day) const override; + void NotifyIsShowing(const base::Feature& feature) override; + void NotifyDismissed(const base::Feature& feature) override; + + private: + // Contains all features that have met conditions within the current session. + std::unordered_set<std::string> shown_features_; + + // Which feature that is currently being shown, or nullptr if nothing is + // currently showing. + std::string currently_showing_feature_; + + DISALLOW_COPY_AND_ASSIGN(OnceConditionValidator); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_ONCE_CONDITION_VALIDATOR_H_ diff --git a/chromium/components/feature_engagement/internal/once_condition_validator_unittest.cc b/chromium/components/feature_engagement/internal/once_condition_validator_unittest.cc new file mode 100644 index 00000000000..cdfc1a6da9d --- /dev/null +++ b/chromium/components/feature_engagement/internal/once_condition_validator_unittest.cc @@ -0,0 +1,155 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/once_condition_validator.h" + +#include <string> + +#include "base/feature_list.h" +#include "components/feature_engagement/internal/editable_configuration.h" +#include "components/feature_engagement/internal/event_model.h" +#include "components/feature_engagement/internal/never_availability_model.h" +#include "components/feature_engagement/internal/proto/event.pb.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +namespace { + +const base::Feature kTestFeatureFoo{"test_foo", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureBar{"test_bar", + base::FEATURE_DISABLED_BY_DEFAULT}; + +FeatureConfig kValidFeatureConfig; +FeatureConfig kInvalidFeatureConfig; + +// A EventModel that is easily configurable at runtime. +class TestEventModel : public EventModel { + public: + TestEventModel() : ready_(false) { kValidFeatureConfig.valid = true; } + + void Initialize(const OnModelInitializationFinished& callback, + uint32_t current_day) override {} + + bool IsReady() const override { return ready_; } + + void SetIsReady(bool ready) { ready_ = ready; } + + const Event* GetEvent(const std::string& event_name) const override { + return nullptr; + } + + void IncrementEvent(const std::string& event_name, uint32_t day) override {} + + private: + bool ready_; +}; + +class OnceConditionValidatorTest : public ::testing::Test { + public: + OnceConditionValidatorTest() { + // By default, event model should be ready. + event_model_.SetIsReady(true); + } + + protected: + EditableConfiguration configuration_; + TestEventModel event_model_; + NeverAvailabilityModel availability_model_; + OnceConditionValidator validator_; + + private: + DISALLOW_COPY_AND_ASSIGN(OnceConditionValidatorTest); +}; + +} // namespace + +TEST_F(OnceConditionValidatorTest, EnabledFeatureShouldTriggerOnce) { + // Only the first call to MeetsConditions() should lead to enlightenment. + EXPECT_TRUE(validator_ + .MeetsConditions(kTestFeatureFoo, kValidFeatureConfig, + event_model_, availability_model_, 0u) + .NoErrors()); + validator_.NotifyIsShowing(kTestFeatureFoo); + ConditionValidator::Result result = + validator_.MeetsConditions(kTestFeatureFoo, kValidFeatureConfig, + event_model_, availability_model_, 0u); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.session_rate_ok); + EXPECT_FALSE(result.trigger_ok); +} + +TEST_F(OnceConditionValidatorTest, + BothEnabledAndDisabledFeaturesShouldTrigger) { + // Only the kTestFeatureFoo feature should lead to enlightenment, since + // kTestFeatureBar is disabled. Ordering disabled feature first to ensure this + // captures a different behavior than the + // OnlyOneFeatureShouldTriggerPerSession test below. + EXPECT_TRUE(validator_ + .MeetsConditions(kTestFeatureBar, kValidFeatureConfig, + event_model_, availability_model_, 0u) + .NoErrors()); + EXPECT_TRUE(validator_ + .MeetsConditions(kTestFeatureFoo, kValidFeatureConfig, + event_model_, availability_model_, 0u) + .NoErrors()); +} + +TEST_F(OnceConditionValidatorTest, StillTriggerWhenAllFeaturesDisabled) { + // No features should get to show enlightenment. + EXPECT_TRUE(validator_ + .MeetsConditions(kTestFeatureFoo, kValidFeatureConfig, + event_model_, availability_model_, 0u) + .NoErrors()); + EXPECT_TRUE(validator_ + .MeetsConditions(kTestFeatureBar, kValidFeatureConfig, + event_model_, availability_model_, 0u) + .NoErrors()); +} + +TEST_F(OnceConditionValidatorTest, OnlyTriggerWhenModelIsReady) { + event_model_.SetIsReady(false); + ConditionValidator::Result result = + validator_.MeetsConditions(kTestFeatureFoo, kValidFeatureConfig, + event_model_, availability_model_, 0u); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.event_model_ready_ok); + + event_model_.SetIsReady(true); + EXPECT_TRUE(validator_ + .MeetsConditions(kTestFeatureFoo, kValidFeatureConfig, + event_model_, availability_model_, 0u) + .NoErrors()); +} + +TEST_F(OnceConditionValidatorTest, OnlyTriggerIfNothingElseIsShowing) { + validator_.NotifyIsShowing(kTestFeatureBar); + ConditionValidator::Result result = + validator_.MeetsConditions(kTestFeatureFoo, kValidFeatureConfig, + event_model_, availability_model_, 0u); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.currently_showing_ok); + + validator_.NotifyDismissed(kTestFeatureBar); + EXPECT_TRUE(validator_ + .MeetsConditions(kTestFeatureFoo, kValidFeatureConfig, + event_model_, availability_model_, 0u) + .NoErrors()); +} + +TEST_F(OnceConditionValidatorTest, DoNotTriggerForInvalidConfig) { + ConditionValidator::Result result = + validator_.MeetsConditions(kTestFeatureFoo, kInvalidFeatureConfig, + event_model_, availability_model_, 0u); + EXPECT_FALSE(result.NoErrors()); + EXPECT_FALSE(result.config_ok); + + EXPECT_TRUE(validator_ + .MeetsConditions(kTestFeatureFoo, kValidFeatureConfig, + event_model_, availability_model_, 0u) + .NoErrors()); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/persistent_availability_store.cc b/chromium/components/feature_engagement/internal/persistent_availability_store.cc new file mode 100644 index 00000000000..d5f4fd0e450 --- /dev/null +++ b/chromium/components/feature_engagement/internal/persistent_availability_store.cc @@ -0,0 +1,158 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/persistent_availability_store.h" + +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/feature_list.h" +#include "base/memory/ptr_util.h" +#include "components/feature_engagement/internal/proto/availability.pb.h" +#include "components/feature_engagement/internal/stats.h" +#include "components/feature_engagement/public/feature_list.h" +#include "components/leveldb_proto/proto_database.h" + +namespace feature_engagement { + +namespace { + +using KeyAvailabilityPair = std::pair<std::string, Availability>; +using KeyAvailabilityList = std::vector<KeyAvailabilityPair>; + +// Corresponds to a UMA suffix "LevelDBOpenResults" in histograms.xml. +// Please do not change. +const char kDatabaseUMAName[] = "FeatureEngagementTrackerAvailabilityStore"; + +void OnDBUpdateComplete( + std::unique_ptr<leveldb_proto::ProtoDatabase<Availability>> db, + PersistentAvailabilityStore::OnLoadedCallback on_loaded_callback, + std::unique_ptr<std::map<std::string, uint32_t>> feature_availabilities, + bool success) { + stats::RecordDbUpdate(success, stats::StoreType::AVAILABILITY_STORE); + std::move(on_loaded_callback).Run(success, std::move(feature_availabilities)); +} + +void OnDBLoadComplete( + std::unique_ptr<leveldb_proto::ProtoDatabase<Availability>> db, + FeatureVector feature_filter, + PersistentAvailabilityStore::OnLoadedCallback on_loaded_callback, + uint32_t current_day, + bool success, + std::unique_ptr<std::vector<Availability>> availabilities) { + stats::RecordAvailabilityDbLoadEvent(success); + if (!success) { + std::move(on_loaded_callback) + .Run(false, base::MakeUnique<std::map<std::string, uint32_t>>()); + return; + } + + // Create map from feature name to Feature. + std::map<std::string, const base::Feature*> feature_mapping; + for (const base::Feature* feature : feature_filter) { + DCHECK(feature_mapping.find(feature->name) == feature_mapping.end()); + feature_mapping[feature->name] = feature; + } + + // Find all availabilities from DB and find out what should be deleted. + auto feature_availabilities = + base::MakeUnique<std::map<std::string, uint32_t>>(); + auto deletes = base::MakeUnique<std::vector<std::string>>(); + for (auto& availability : *availabilities) { + // Check if in |feature_filter|. + if (feature_mapping.find(availability.feature_name()) == + feature_mapping.end()) { + deletes->push_back(availability.feature_name()); + continue; + } + + // Check if enabled. + const base::Feature* feature = feature_mapping[availability.feature_name()]; + if (!base::FeatureList::IsEnabled(*feature)) { + deletes->push_back(availability.feature_name()); + continue; + } + + // Both in |feature_filter| and is enabled, so keep around. + feature_availabilities->insert( + std::make_pair(feature->name, availability.day())); + DVLOG(2) << "Keeping availability for " << feature->name << " @ " + << availability.day(); + } + + // Find features from |feature_filter| that are enabled, but not in DB yet. + auto additions = base::MakeUnique<KeyAvailabilityList>(); + for (const base::Feature* feature : feature_filter) { + // Check if already in DB. + if (feature_availabilities->find(feature->name) != + feature_availabilities->end()) + continue; + + // Check if enabled. + if (!base::FeatureList::IsEnabled(*feature)) + continue; + + // Both in feature filter, and is enabled, but not in DB, so add to DB. + Availability availability; + availability.set_feature_name(feature->name); + availability.set_day(current_day); + additions->push_back( + std::make_pair(availability.feature_name(), std::move(availability))); + + // Since it will be written to the DB, also add to the callback result. + feature_availabilities->insert( + std::make_pair(feature->name, availability.day())); + DVLOG(2) << "Adding availability for " << feature->name << " @ " + << availability.day(); + } + + // Write all changes to the DB. + auto* db_ptr = db.get(); + db_ptr->UpdateEntries(std::move(additions), std::move(deletes), + base::BindOnce(&OnDBUpdateComplete, std::move(db), + std::move(on_loaded_callback), + std::move(feature_availabilities))); +} + +void OnDBInitComplete( + std::unique_ptr<leveldb_proto::ProtoDatabase<Availability>> db, + FeatureVector feature_filter, + PersistentAvailabilityStore::OnLoadedCallback on_loaded_callback, + uint32_t current_day, + bool success) { + stats::RecordDbInitEvent(success, stats::StoreType::AVAILABILITY_STORE); + + if (!success) { + std::move(on_loaded_callback) + .Run(false, base::MakeUnique<std::map<std::string, uint32_t>>()); + return; + } + + auto* db_ptr = db.get(); + db_ptr->LoadEntries(base::BindOnce( + &OnDBLoadComplete, std::move(db), std::move(feature_filter), + std::move(on_loaded_callback), current_day)); +} + +} // namespace + +// static +void PersistentAvailabilityStore::LoadAndUpdateStore( + const base::FilePath& storage_dir, + std::unique_ptr<leveldb_proto::ProtoDatabase<Availability>> db, + FeatureVector feature_filter, + PersistentAvailabilityStore::OnLoadedCallback on_loaded_callback, + uint32_t current_day) { + auto* db_ptr = db.get(); + db_ptr->Init(kDatabaseUMAName, storage_dir, + base::BindOnce(&OnDBInitComplete, std::move(db), + std::move(feature_filter), + std::move(on_loaded_callback), current_day)); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/persistent_availability_store.h b/chromium/components/feature_engagement/internal/persistent_availability_store.h new file mode 100644 index 00000000000..808d0cfb3fb --- /dev/null +++ b/chromium/components/feature_engagement/internal/persistent_availability_store.h @@ -0,0 +1,61 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_PERSISTENT_AVAILABILITY_STORE_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_PERSISTENT_AVAILABILITY_STORE_H_ + +#include <stdint.h> + +#include <map> +#include <memory> + +#include "base/callback_forward.h" +#include "base/macros.h" +#include "components/feature_engagement/internal/proto/availability.pb.h" +#include "components/feature_engagement/public/feature_list.h" +#include "components/leveldb_proto/proto_database.h" + +namespace base { +class FilePath; +} // namespace base + +namespace feature_engagement { + +// An PersistentAvailabilityStore provides a way to load and update the +// availability date for all registered features. +class PersistentAvailabilityStore { + public: + // Invoked when the availability data has finished loading, and whether the + // load was a success. In the case of a failure, the map argument will be + // empty. The value for each entry in the map is the day number since epoch + // (1970-01-01) in the local timezone for when the particular feature was made + // available. + using OnLoadedCallback = base::OnceCallback< + void(bool success, std::unique_ptr<std::map<std::string, uint32_t>>)>; + + // Loads the availability data, updates the DB with newly enabled features, + // deletes features that are not enabled anymore, and asynchronously invokes + // |on_loaded_callback| with the result. The result will mirror the content + // of the database. + // The |feature_filter| is used to filter the data from the DB and ensure + // that only enabled features listed in this filter are tracked. For enabled + // features that are in the |feature_filter|, but not in the DB, they are + // tracked as new entries with the |current_day| as the availability day. + static void LoadAndUpdateStore( + const base::FilePath& storage_dir, + std::unique_ptr<leveldb_proto::ProtoDatabase<Availability>> db, + FeatureVector feature_filter, + OnLoadedCallback on_loaded_callback, + uint32_t current_day); + + private: + PersistentAvailabilityStore() = default; + ~PersistentAvailabilityStore() = default; + + DISALLOW_COPY_AND_ASSIGN(PersistentAvailabilityStore); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_PERSISTENT_AVAILABILITY_STORE_H_ diff --git a/chromium/components/feature_engagement/internal/persistent_availability_store_unittest.cc b/chromium/components/feature_engagement/internal/persistent_availability_store_unittest.cc new file mode 100644 index 00000000000..8e204e92bae --- /dev/null +++ b/chromium/components/feature_engagement/internal/persistent_availability_store_unittest.cc @@ -0,0 +1,295 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/persistent_availability_store.h" + +#include <stdint.h> + +#include <map> +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/macros.h" +#include "base/memory/ptr_util.h" +#include "base/optional.h" +#include "base/test/scoped_feature_list.h" +#include "components/feature_engagement/internal/proto/availability.pb.h" +#include "components/feature_engagement/public/feature_list.h" +#include "components/leveldb_proto/proto_database.h" +#include "components/leveldb_proto/testing/fake_db.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +namespace { +const base::Feature kTestFeatureFoo{"test_foo", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureBar{"test_bar", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureQux{"test_qux", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureNop{"test_nop", + base::FEATURE_DISABLED_BY_DEFAULT}; + +Availability CreateAvailability(const base::Feature& feature, uint32_t day) { + Availability availability; + availability.set_feature_name(feature.name); + availability.set_day(day); + return availability; +} + +class PersistentAvailabilityStoreTest : public testing::Test { + public: + PersistentAvailabilityStoreTest() + : db_(nullptr), + storage_dir_(FILE_PATH_LITERAL("/persistent/store/lalala")) { + load_callback_ = base::Bind(&PersistentAvailabilityStoreTest::LoadCallback, + base::Unretained(this)); + } + + ~PersistentAvailabilityStoreTest() override = default; + + // Creates a DB and stores off a pointer to it as a member. + std::unique_ptr<leveldb_proto::test::FakeDB<Availability>> CreateDB() { + auto db = base::MakeUnique<leveldb_proto::test::FakeDB<Availability>>( + &db_availabilities_); + db_ = db.get(); + return db; + } + + void LoadCallback( + bool success, + std::unique_ptr<std::map<std::string, uint32_t>> availabilities) { + load_successful_ = success; + load_results_ = std::move(availabilities); + } + + protected: + base::test::ScopedFeatureList scoped_feature_list_; + + // The end result of the store pipeline. + PersistentAvailabilityStore::OnLoadedCallback load_callback_; + + // Callback results. + base::Optional<bool> load_successful_; + std::unique_ptr<std::map<std::string, uint32_t>> load_results_; + + // |db_availabilities_| is used during creation of the FakeDB in CreateDB(), + // to simplify what the DB has stored. + std::map<std::string, Availability> db_availabilities_; + + // The database that is in use. + leveldb_proto::test::FakeDB<Availability>* db_; + + // Constant test data. + base::FilePath storage_dir_; + + private: + DISALLOW_COPY_AND_ASSIGN(PersistentAvailabilityStoreTest); +}; + +} // namespace + +TEST_F(PersistentAvailabilityStoreTest, StorageDirectory) { + PersistentAvailabilityStore::LoadAndUpdateStore( + storage_dir_, CreateDB(), FeatureVector(), std::move(load_callback_), + 14u); + db_->InitCallback(true); + EXPECT_EQ(storage_dir_, db_->GetDirectory()); + + // Finish the pipeline to ensure the test does not leak anything. + db_->LoadCallback(false); +} + +TEST_F(PersistentAvailabilityStoreTest, InitFail) { + PersistentAvailabilityStore::LoadAndUpdateStore( + storage_dir_, CreateDB(), FeatureVector(), std::move(load_callback_), + 14u); + + db_->InitCallback(false); + + EXPECT_TRUE(load_successful_.has_value()); + EXPECT_FALSE(load_successful_.value()); + EXPECT_EQ(0u, load_results_->size()); + EXPECT_EQ(0u, db_availabilities_.size()); +} + +TEST_F(PersistentAvailabilityStoreTest, LoadFail) { + PersistentAvailabilityStore::LoadAndUpdateStore( + storage_dir_, CreateDB(), FeatureVector(), std::move(load_callback_), + 14u); + + db_->InitCallback(true); + EXPECT_FALSE(load_successful_.has_value()); + + db_->LoadCallback(false); + + EXPECT_TRUE(load_successful_.has_value()); + EXPECT_FALSE(load_successful_.value()); + EXPECT_EQ(0u, load_results_->size()); + EXPECT_EQ(0u, db_availabilities_.size()); +} + +TEST_F(PersistentAvailabilityStoreTest, EmptyDBEmptyFeatureFilterUpdateFailed) { + PersistentAvailabilityStore::LoadAndUpdateStore( + storage_dir_, CreateDB(), FeatureVector(), std::move(load_callback_), + 14u); + + db_->InitCallback(true); + EXPECT_FALSE(load_successful_.has_value()); + + db_->LoadCallback(true); + EXPECT_FALSE(load_successful_.has_value()); + + db_->UpdateCallback(false); + + EXPECT_TRUE(load_successful_.has_value()); + EXPECT_FALSE(load_successful_.value()); + EXPECT_EQ(0u, load_results_->size()); + EXPECT_EQ(0u, db_availabilities_.size()); +} + +TEST_F(PersistentAvailabilityStoreTest, EmptyDBEmptyFeatureFilterUpdateOK) { + PersistentAvailabilityStore::LoadAndUpdateStore( + storage_dir_, CreateDB(), FeatureVector(), std::move(load_callback_), + 14u); + + db_->InitCallback(true); + EXPECT_FALSE(load_successful_.has_value()); + + db_->LoadCallback(true); + EXPECT_FALSE(load_successful_.has_value()); + + db_->UpdateCallback(true); + + EXPECT_TRUE(load_successful_.has_value()); + EXPECT_TRUE(load_successful_.value()); + EXPECT_EQ(0u, load_results_->size()); + EXPECT_EQ(0u, db_availabilities_.size()); +} + +TEST_F(PersistentAvailabilityStoreTest, AllNewFeatures) { + scoped_feature_list_.InitWithFeatures({kTestFeatureFoo, kTestFeatureBar}, + {kTestFeatureQux}); + + FeatureVector feature_filter; + feature_filter.push_back(&kTestFeatureFoo); // Enabled. Not in DB. + feature_filter.push_back(&kTestFeatureBar); // Enabled. Not in DB. + feature_filter.push_back(&kTestFeatureQux); // Disabled. Not in DB. + + PersistentAvailabilityStore::LoadAndUpdateStore( + storage_dir_, CreateDB(), feature_filter, std::move(load_callback_), 14u); + + db_->InitCallback(true); + EXPECT_FALSE(load_successful_.has_value()); + + db_->LoadCallback(true); + EXPECT_FALSE(load_successful_.has_value()); + + db_->UpdateCallback(true); + + EXPECT_TRUE(load_successful_.has_value()); + EXPECT_TRUE(load_successful_.value()); + ASSERT_EQ(2u, load_results_->size()); + ASSERT_EQ(2u, db_availabilities_.size()); + + ASSERT_TRUE(load_results_->find(kTestFeatureFoo.name) != + load_results_->end()); + EXPECT_EQ(14u, (*load_results_)[kTestFeatureFoo.name]); + ASSERT_TRUE(db_availabilities_.find(kTestFeatureFoo.name) != + db_availabilities_.end()); + EXPECT_EQ(14u, db_availabilities_[kTestFeatureFoo.name].day()); + + ASSERT_TRUE(load_results_->find(kTestFeatureBar.name) != + load_results_->end()); + EXPECT_EQ(14u, (*load_results_)[kTestFeatureBar.name]); + ASSERT_TRUE(db_availabilities_.find(kTestFeatureBar.name) != + db_availabilities_.end()); + EXPECT_EQ(14u, db_availabilities_[kTestFeatureBar.name].day()); +} + +TEST_F(PersistentAvailabilityStoreTest, TestAllFilterCombinations) { + scoped_feature_list_.InitWithFeatures({kTestFeatureFoo, kTestFeatureBar}, + {kTestFeatureQux, kTestFeatureNop}); + + FeatureVector feature_filter; + feature_filter.push_back(&kTestFeatureFoo); // Enabled. Not in DB. + feature_filter.push_back(&kTestFeatureBar); // Enabled. In DB. + feature_filter.push_back(&kTestFeatureQux); // Disabled. Not in DB. + feature_filter.push_back(&kTestFeatureNop); // Disabled. In DB. + + db_availabilities_[kTestFeatureBar.name] = + CreateAvailability(kTestFeatureBar, 10u); + db_availabilities_[kTestFeatureNop.name] = + CreateAvailability(kTestFeatureNop, 8u); + + PersistentAvailabilityStore::LoadAndUpdateStore( + storage_dir_, CreateDB(), feature_filter, std::move(load_callback_), 14u); + + db_->InitCallback(true); + EXPECT_FALSE(load_successful_.has_value()); + + db_->LoadCallback(true); + EXPECT_FALSE(load_successful_.has_value()); + + db_->UpdateCallback(true); + + EXPECT_TRUE(load_successful_.has_value()); + EXPECT_TRUE(load_successful_.value()); + ASSERT_EQ(2u, load_results_->size()); + ASSERT_EQ(2u, db_availabilities_.size()); + + ASSERT_TRUE(load_results_->find(kTestFeatureFoo.name) != + load_results_->end()); + EXPECT_EQ(14u, (*load_results_)[kTestFeatureFoo.name]); + ASSERT_TRUE(db_availabilities_.find(kTestFeatureFoo.name) != + db_availabilities_.end()); + EXPECT_EQ(14u, db_availabilities_[kTestFeatureFoo.name].day()); + + ASSERT_TRUE(load_results_->find(kTestFeatureBar.name) != + load_results_->end()); + EXPECT_EQ(10u, (*load_results_)[kTestFeatureBar.name]); + ASSERT_TRUE(db_availabilities_.find(kTestFeatureBar.name) != + db_availabilities_.end()); + EXPECT_EQ(10u, db_availabilities_[kTestFeatureBar.name].day()); +} + +TEST_F(PersistentAvailabilityStoreTest, TestAllCombinationsEmptyFilter) { + scoped_feature_list_.InitWithFeatures({kTestFeatureFoo, kTestFeatureBar}, + {kTestFeatureQux, kTestFeatureNop}); + + // Empty filter, but the following setup: + // kTestFeatureFoo: Enabled. Not in DB. + // kTestFeatureBar: Enabled. In DB. + // kTestFeatureQux: Disabled. Not in DB. + // kTestFeatureNop: Disabled. In DB. + + db_availabilities_[kTestFeatureBar.name] = + CreateAvailability(kTestFeatureBar, 10u); + db_availabilities_[kTestFeatureNop.name] = + CreateAvailability(kTestFeatureNop, 8u); + + PersistentAvailabilityStore::LoadAndUpdateStore( + storage_dir_, CreateDB(), FeatureVector(), std::move(load_callback_), + 14u); + + db_->InitCallback(true); + EXPECT_FALSE(load_successful_.has_value()); + + db_->LoadCallback(true); + EXPECT_FALSE(load_successful_.has_value()); + + db_->UpdateCallback(true); + + EXPECT_TRUE(load_successful_.has_value()); + EXPECT_TRUE(load_successful_.value()); + EXPECT_EQ(0u, load_results_->size()); + EXPECT_EQ(0u, db_availabilities_.size()); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/persistent_event_store.cc b/chromium/components/feature_engagement/internal/persistent_event_store.cc new file mode 100644 index 00000000000..6dfccc25a70 --- /dev/null +++ b/chromium/components/feature_engagement/internal/persistent_event_store.cc @@ -0,0 +1,91 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/persistent_event_store.h" + +#include <vector> + +#include "base/bind.h" +#include "base/memory/ptr_util.h" +#include "components/feature_engagement/internal/stats.h" + +namespace feature_engagement { +namespace { +// Corresponds to a UMA suffix "LevelDBOpenResults" in histograms.xml. +// Please do not change. +const char kDatabaseUMAName[] = "FeatureEngagementTrackerEventStore"; + +using KeyEventPair = std::pair<std::string, Event>; +using KeyEventList = std::vector<KeyEventPair>; + +void NoopUpdateCallback(bool success) { + stats::RecordDbUpdate(success, stats::StoreType::EVENTS_STORE); +} + +} // namespace + +PersistentEventStore::PersistentEventStore( + const base::FilePath& storage_dir, + std::unique_ptr<leveldb_proto::ProtoDatabase<Event>> db) + : storage_dir_(storage_dir), + db_(std::move(db)), + ready_(false), + weak_ptr_factory_(this) {} + +PersistentEventStore::~PersistentEventStore() = default; + +void PersistentEventStore::Load(const OnLoadedCallback& callback) { + DCHECK(!ready_); + + db_->Init(kDatabaseUMAName, storage_dir_, + base::Bind(&PersistentEventStore::OnInitComplete, + weak_ptr_factory_.GetWeakPtr(), callback)); +} + +bool PersistentEventStore::IsReady() const { + return ready_; +} + +void PersistentEventStore::WriteEvent(const Event& event) { + DCHECK(IsReady()); + std::unique_ptr<KeyEventList> entries = base::MakeUnique<KeyEventList>(); + entries->push_back(KeyEventPair(event.name(), event)); + + db_->UpdateEntries(std::move(entries), + base::MakeUnique<std::vector<std::string>>(), + base::Bind(&NoopUpdateCallback)); +} + +void PersistentEventStore::DeleteEvent(const std::string& event_name) { + DCHECK(IsReady()); + auto deletes = base::MakeUnique<std::vector<std::string>>(); + deletes->push_back(event_name); + + db_->UpdateEntries(base::MakeUnique<KeyEventList>(), std::move(deletes), + base::Bind(&NoopUpdateCallback)); +} + +void PersistentEventStore::OnInitComplete(const OnLoadedCallback& callback, + bool success) { + stats::RecordDbInitEvent(success, stats::StoreType::EVENTS_STORE); + + if (!success) { + callback.Run(false, base::MakeUnique<std::vector<Event>>()); + return; + } + + db_->LoadEntries(base::Bind(&PersistentEventStore::OnLoadComplete, + weak_ptr_factory_.GetWeakPtr(), callback)); +} + +void PersistentEventStore::OnLoadComplete( + const OnLoadedCallback& callback, + bool success, + std::unique_ptr<std::vector<Event>> entries) { + stats::RecordEventDbLoadEvent(success, *entries.get()); + ready_ = success; + callback.Run(success, std::move(entries)); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/persistent_event_store.h b/chromium/components/feature_engagement/internal/persistent_event_store.h new file mode 100644 index 00000000000..753ced32af3 --- /dev/null +++ b/chromium/components/feature_engagement/internal/persistent_event_store.h @@ -0,0 +1,59 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_PERSISTENT_EVENT_STORE_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_PERSISTENT_EVENT_STORE_H_ + +#include <memory> +#include <vector> + +#include "base/files/file_path.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "components/feature_engagement/internal/event_store.h" +#include "components/feature_engagement/internal/proto/event.pb.h" +#include "components/leveldb_proto/proto_database.h" + +namespace feature_engagement { + +// A PersistentEventStore provides a DB layer that persists the data to disk. +// The data is retrieved once during the load process and after that this store +// is write only. Data will be persisted asynchronously so it is not guaranteed +// to always save every write during shutdown. +class PersistentEventStore : public EventStore { + public: + // Builds a PersistentEventStore backed by the ProtoDatabase |db|. The + // database will be loaded and/or created at |storage_dir|. + PersistentEventStore(const base::FilePath& storage_dir, + std::unique_ptr<leveldb_proto::ProtoDatabase<Event>> db); + ~PersistentEventStore() override; + + // EventStore implementation. + void Load(const OnLoadedCallback& callback) override; + bool IsReady() const override; + void WriteEvent(const Event& event) override; + void DeleteEvent(const std::string& event_name) override; + + private: + void OnInitComplete(const OnLoadedCallback& callback, bool success); + void OnLoadComplete(const OnLoadedCallback& callback, + bool success, + std::unique_ptr<std::vector<Event>> entries); + + const base::FilePath storage_dir_; + std::unique_ptr<leveldb_proto::ProtoDatabase<Event>> db_; + + // Whether or not the underlying ProtoDatabase is ready. This will be false + // until the OnLoadedCallback is broadcast. It will also be false if loading + // fails. + bool ready_; + + base::WeakPtrFactory<PersistentEventStore> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(PersistentEventStore); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_PERSISTENT_EVENT_STORE_H_ diff --git a/chromium/components/feature_engagement/internal/persistent_event_store_unittest.cc b/chromium/components/feature_engagement/internal/persistent_event_store_unittest.cc new file mode 100644 index 00000000000..46e48cd7a95 --- /dev/null +++ b/chromium/components/feature_engagement/internal/persistent_event_store_unittest.cc @@ -0,0 +1,251 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/persistent_event_store.h" + +#include <map> + +#include "base/files/file_path.h" +#include "base/memory/ptr_util.h" +#include "base/optional.h" +#include "base/test/histogram_tester.h" +#include "components/feature_engagement/internal/proto/event.pb.h" +#include "components/feature_engagement/internal/stats.h" +#include "components/feature_engagement/internal/test/event_util.h" +#include "components/leveldb_proto/proto_database.h" +#include "components/leveldb_proto/testing/fake_db.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +namespace { + +void VerifyEventsInListAndMap(const std::map<std::string, Event>& map, + const std::vector<Event>& list) { + ASSERT_EQ(map.size(), list.size()); + + for (const auto& event : list) { + const auto& it = map.find(event.name()); + ASSERT_NE(map.end(), it); + test::VerifyEventsEqual(&event, &it->second); + } +} + +class PersistentEventStoreTest : public ::testing::Test { + public: + PersistentEventStoreTest() + : db_(nullptr), + storage_dir_(FILE_PATH_LITERAL("/persistent/store/lalala")) { + load_callback_ = base::Bind(&PersistentEventStoreTest::LoadCallback, + base::Unretained(this)); + } + + void TearDown() override { + db_events_.clear(); + db_ = nullptr; + store_.reset(); + } + + protected: + void SetUpDB() { + DCHECK(!db_); + DCHECK(!store_); + + auto db = base::MakeUnique<leveldb_proto::test::FakeDB<Event>>(&db_events_); + db_ = db.get(); + store_.reset(new PersistentEventStore(storage_dir_, std::move(db))); + } + + void LoadCallback(bool success, std::unique_ptr<std::vector<Event>> events) { + load_successful_ = success; + load_results_ = std::move(events); + } + + // Callback results. + base::Optional<bool> load_successful_; + std::unique_ptr<std::vector<Event>> load_results_; + + EventStore::OnLoadedCallback load_callback_; + std::map<std::string, Event> db_events_; + leveldb_proto::test::FakeDB<Event>* db_; + std::unique_ptr<EventStore> store_; + + // Constant test data. + base::FilePath storage_dir_; +}; + +} // namespace + +TEST_F(PersistentEventStoreTest, StorageDirectory) { + SetUpDB(); + store_->Load(load_callback_); + EXPECT_EQ(storage_dir_, db_->GetDirectory()); +} + +TEST_F(PersistentEventStoreTest, SuccessfulInitAndLoadEmptyStore) { + SetUpDB(); + + base::HistogramTester histogram_tester; + + store_->Load(load_callback_); + // The initialize should not trigger a response to the callback. + db_->InitCallback(true); + EXPECT_FALSE(load_successful_.has_value()); + + // The load should trigger a response to the callback. + db_->LoadCallback(true); + EXPECT_TRUE(load_successful_.value()); + + // Validate that we have no entries. + EXPECT_NE(nullptr, load_results_); + EXPECT_TRUE(load_results_->empty()); + + // Verify histograms. + std::string suffix = + stats::ToDbHistogramSuffix(stats::StoreType::EVENTS_STORE); + histogram_tester.ExpectBucketCount("InProductHelp.Db.Init." + suffix, 1, 1); + histogram_tester.ExpectBucketCount("InProductHelp.Db.Load." + suffix, 1, 1); + histogram_tester.ExpectBucketCount("InProductHelp.Db.TotalEvents", 0, 1); +} + +TEST_F(PersistentEventStoreTest, SuccessfulInitAndLoadWithEvents) { + // Populate fake Event entries. + Event event1; + event1.set_name("event1"); + test::SetEventCountForDay(&event1, 1, 1); + + Event event2; + event2.set_name("event2"); + test::SetEventCountForDay(&event2, 1, 3); + test::SetEventCountForDay(&event2, 2, 5); + + db_events_.insert(std::pair<std::string, Event>(event1.name(), event1)); + db_events_.insert(std::pair<std::string, Event>(event2.name(), event2)); + + SetUpDB(); + + base::HistogramTester histogram_tester; + + // The initialize should not trigger a response to the callback. + store_->Load(load_callback_); + db_->InitCallback(true); + EXPECT_FALSE(load_successful_.has_value()); + + // The load should trigger a response to the callback. + db_->LoadCallback(true); + EXPECT_TRUE(load_successful_.value()); + EXPECT_NE(nullptr, load_results_); + + // Validate that we have the two events that we expect. + VerifyEventsInListAndMap(db_events_, *load_results_); + + // Verify histograms. + std::string suffix = + stats::ToDbHistogramSuffix(stats::StoreType::EVENTS_STORE); + histogram_tester.ExpectBucketCount("InProductHelp.Db.Init." + suffix, 1, 1); + histogram_tester.ExpectBucketCount("InProductHelp.Db.Load." + suffix, 1, 1); + histogram_tester.ExpectBucketCount("InProductHelp.Db.TotalEvents", 3, 1); +} + +TEST_F(PersistentEventStoreTest, SuccessfulInitBadLoad) { + base::HistogramTester histogram_tester; + SetUpDB(); + + store_->Load(load_callback_); + + // The initialize should not trigger a response to the callback. + db_->InitCallback(true); + EXPECT_FALSE(load_successful_.has_value()); + + // The load will fail and should trigger the callback. + db_->LoadCallback(false); + EXPECT_FALSE(load_successful_.value()); + EXPECT_FALSE(store_->IsReady()); + + // Histograms. + std::string suffix = + stats::ToDbHistogramSuffix(stats::StoreType::EVENTS_STORE); + histogram_tester.ExpectBucketCount("InProductHelp.Db.Init." + suffix, 1, 1); + histogram_tester.ExpectBucketCount("InProductHelp.Db.Load." + suffix, 0, 1); + histogram_tester.ExpectTotalCount("InProductHelp.Db.TotalEvents", 0); +} + +TEST_F(PersistentEventStoreTest, BadInit) { + base::HistogramTester histogram_tester; + SetUpDB(); + + store_->Load(load_callback_); + + // The initialize will fail and should trigger the callback. + db_->InitCallback(false); + EXPECT_FALSE(load_successful_.value()); + EXPECT_FALSE(store_->IsReady()); + + // Histograms. + std::string suffix = + stats::ToDbHistogramSuffix(stats::StoreType::EVENTS_STORE); + histogram_tester.ExpectBucketCount("InProductHelp.Db.Init." + suffix, 0, 1); + histogram_tester.ExpectTotalCount("InProductHelp.Db.Load." + suffix, 0); + histogram_tester.ExpectTotalCount("InProductHelp.Db.TotalEvents", 0); +} + +TEST_F(PersistentEventStoreTest, IsReady) { + SetUpDB(); + EXPECT_FALSE(store_->IsReady()); + + store_->Load(load_callback_); + EXPECT_FALSE(store_->IsReady()); + + db_->InitCallback(true); + EXPECT_FALSE(store_->IsReady()); + + db_->LoadCallback(true); + EXPECT_TRUE(store_->IsReady()); +} + +TEST_F(PersistentEventStoreTest, WriteEvent) { + SetUpDB(); + + store_->Load(load_callback_); + db_->InitCallback(true); + db_->LoadCallback(true); + + Event event; + event.set_name("event"); + test::SetEventCountForDay(&event, 1, 2); + + store_->WriteEvent(event); + db_->UpdateCallback(true); + + EXPECT_EQ(1U, db_events_.size()); + + const auto& it = db_events_.find("event"); + EXPECT_NE(db_events_.end(), it); + test::VerifyEventsEqual(&event, &it->second); +} + +TEST_F(PersistentEventStoreTest, WriteAndDeleteEvent) { + SetUpDB(); + + store_->Load(load_callback_); + db_->InitCallback(true); + db_->LoadCallback(true); + + Event event; + event.set_name("event"); + test::SetEventCountForDay(&event, 1, 2); + + store_->WriteEvent(event); + db_->UpdateCallback(true); + + EXPECT_EQ(1U, db_events_.size()); + + store_->DeleteEvent("event"); + db_->UpdateCallback(true); + + const auto& it = db_events_.find("event"); + EXPECT_EQ(db_events_.end(), it); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/proto/BUILD.gn b/chromium/components/feature_engagement/internal/proto/BUILD.gn new file mode 100644 index 00000000000..e4063482e90 --- /dev/null +++ b/chromium/components/feature_engagement/internal/proto/BUILD.gn @@ -0,0 +1,12 @@ +# Copyright 2017 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import("//third_party/protobuf/proto_library.gni") + +proto_library("proto") { + sources = [ + "availability.proto", + "event.proto", + ] +} diff --git a/chromium/components/feature_engagement/internal/proto/availability.proto b/chromium/components/feature_engagement/internal/proto/availability.proto new file mode 100644 index 00000000000..cc389da159c --- /dev/null +++ b/chromium/components/feature_engagement/internal/proto/availability.proto @@ -0,0 +1,21 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// feature_engagement::AvailabilityModel content. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; + +package feature_engagement; + +// Availability stores state for the availability of a particular feature. +message Availability { + // The name of the feature. Must match base::Feature::name. + optional string feature_name = 1; + + // The day number since epoch (1970-01-01) in the local timezone for when the + // particular |feature| was made available. + optional uint32 day = 2; +} diff --git a/chromium/components/feature_engagement/internal/proto/event.proto b/chromium/components/feature_engagement/internal/proto/event.proto new file mode 100644 index 00000000000..9f59b5266f5 --- /dev/null +++ b/chromium/components/feature_engagement/internal/proto/event.proto @@ -0,0 +1,27 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// feature_engagement::EventModel content. + +syntax = "proto2"; + +option optimize_for = LITE_RUNTIME; + +package feature_engagement; + +// Event stores state for a specific event a count per day it has happened. +message Event { + // Count stores a pair of a day and how many times something happened that + // day. + message Count { + optional uint32 day = 1; + optional uint32 count = 2; + } + + // The descriptive name of the event. + optional string name = 1; + + // The number of this event that happened per day. + repeated Count events = 2; +} diff --git a/chromium/components/feature_engagement/internal/single_invalid_configuration.cc b/chromium/components/feature_engagement/internal/single_invalid_configuration.cc new file mode 100644 index 00000000000..5d06652ac8b --- /dev/null +++ b/chromium/components/feature_engagement/internal/single_invalid_configuration.cc @@ -0,0 +1,33 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/single_invalid_configuration.h" + +#include "components/feature_engagement/internal/configuration.h" + +namespace feature_engagement { + +SingleInvalidConfiguration::SingleInvalidConfiguration() { + invalid_feature_config_.valid = false; + invalid_feature_config_.used.name = "nothing_to_see_here"; +} + +SingleInvalidConfiguration::~SingleInvalidConfiguration() = default; + +const FeatureConfig& SingleInvalidConfiguration::GetFeatureConfig( + const base::Feature& feature) const { + return invalid_feature_config_; +} + +const FeatureConfig& SingleInvalidConfiguration::GetFeatureConfigByName( + const std::string& feature_name) const { + return invalid_feature_config_; +} + +const Configuration::ConfigMap& +SingleInvalidConfiguration::GetRegisteredFeatures() const { + return configs_; +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/single_invalid_configuration.h b/chromium/components/feature_engagement/internal/single_invalid_configuration.h new file mode 100644 index 00000000000..de6ce42caa6 --- /dev/null +++ b/chromium/components/feature_engagement/internal/single_invalid_configuration.h @@ -0,0 +1,46 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_SINGLE_INVALID_CONFIGURATION_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_SINGLE_INVALID_CONFIGURATION_H_ + +#include <string> +#include <unordered_set> + +#include "base/macros.h" +#include "components/feature_engagement/internal/configuration.h" + +namespace base { +struct Feature; +} // namespace base + +namespace feature_engagement { + +// An Configuration that always returns the same single invalid configuration, +// regardless of which feature. Also holds an empty ConfigMap. +class SingleInvalidConfiguration : public Configuration { + public: + SingleInvalidConfiguration(); + ~SingleInvalidConfiguration() override; + + // Configuration implementation. + const FeatureConfig& GetFeatureConfig( + const base::Feature& feature) const override; + const FeatureConfig& GetFeatureConfigByName( + const std::string& feature_name) const override; + const Configuration::ConfigMap& GetRegisteredFeatures() const override; + + private: + // The invalid configuration to always return. + FeatureConfig invalid_feature_config_; + + // An empty map. + ConfigMap configs_; + + DISALLOW_COPY_AND_ASSIGN(SingleInvalidConfiguration); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_SINGLE_INVALID_CONFIGURATION_H_ diff --git a/chromium/components/feature_engagement/internal/single_invalid_configuration_unittest.cc b/chromium/components/feature_engagement/internal/single_invalid_configuration_unittest.cc new file mode 100644 index 00000000000..c8f369ce07d --- /dev/null +++ b/chromium/components/feature_engagement/internal/single_invalid_configuration_unittest.cc @@ -0,0 +1,41 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/single_invalid_configuration.h" + +#include "base/feature_list.h" +#include "components/feature_engagement/internal/configuration.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +namespace { + +const base::Feature kTestFeatureFoo{"test_foo", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureBar{"test_bar", + base::FEATURE_DISABLED_BY_DEFAULT}; + +class SingleInvalidConfigurationTest : public ::testing::Test { + public: + SingleInvalidConfigurationTest() = default; + + protected: + SingleInvalidConfiguration configuration_; + + private: + DISALLOW_COPY_AND_ASSIGN(SingleInvalidConfigurationTest); +}; + +} // namespace + +TEST_F(SingleInvalidConfigurationTest, AllConfigurationsAreInvalid) { + FeatureConfig foo_config = configuration_.GetFeatureConfig(kTestFeatureFoo); + EXPECT_FALSE(foo_config.valid); + + FeatureConfig bar_config = configuration_.GetFeatureConfig(kTestFeatureBar); + EXPECT_FALSE(bar_config.valid); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/stats.cc b/chromium/components/feature_engagement/internal/stats.cc new file mode 100644 index 00000000000..5cc79b2604c --- /dev/null +++ b/chromium/components/feature_engagement/internal/stats.cc @@ -0,0 +1,210 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/stats.h" + +#include <string> + +#include "base/metrics/histogram_functions.h" +#include "base/metrics/histogram_macros.h" +#include "base/metrics/user_metrics.h" +#include "components/feature_engagement/public/feature_list.h" + +namespace feature_engagement { +namespace stats { +namespace { + +// Histogram suffixes for database metrics, must match the ones in +// histograms.xml. +const char kEventStoreSuffix[] = "EventStore"; +const char kAvailabilityStoreSuffix[] = "AvailabilityStore"; + +// A shadow histogram across all features. Also the base name for the suffix +// based feature specific histograms; for example for IPH_MyFun, it would be: +// InProductHelp.ShouldTriggerHelpUI.IPH_MyFun. +const char kShouldTriggerHelpUIHistogram[] = + "InProductHelp.ShouldTriggerHelpUI"; + +// Helper function to log a TriggerHelpUIResult. +void LogTriggerHelpUIResult(const std::string& name, + TriggerHelpUIResult result) { + // Must not use histograms macros here because we pass in the histogram name. + base::UmaHistogramEnumeration(name, result, TriggerHelpUIResult::COUNT); + base::UmaHistogramEnumeration(kShouldTriggerHelpUIHistogram, result, + TriggerHelpUIResult::COUNT); +} + +} // namespace + +std::string ToDbHistogramSuffix(StoreType type) { + switch (type) { + case StoreType::EVENTS_STORE: + return std::string(kEventStoreSuffix); + case StoreType::AVAILABILITY_STORE: + return std::string(kAvailabilityStoreSuffix); + default: + NOTREACHED(); + return std::string(); + } +} + +void RecordNotifyEvent(const std::string& event_name, + const Configuration* config, + bool is_model_ready) { + DCHECK(!event_name.empty()); + DCHECK(config); + + // Find which feature this event belongs to. + const Configuration::ConfigMap& features = config->GetRegisteredFeatures(); + std::string feature_name; + for (const auto& element : features) { + const std::string fname = element.first; + const FeatureConfig& feature_config = element.second; + + // Track used event separately. + if (feature_config.used.name == event_name) { + feature_name = fname; + DCHECK(!feature_name.empty()); + std::string used_event_action = "InProductHelp.NotifyUsedEvent."; + used_event_action.append(feature_name); + base::RecordComputedAction(used_event_action); + break; + } + + // Find if the |event_name| matches any configuration. + for (const auto& event : feature_config.event_configs) { + if (event.name == event_name) { + feature_name = fname; + break; + } + } + if (feature_config.trigger.name == event_name) { + feature_name = fname; + break; + } + } + + // Do nothing if no events in the configuration matches the |event_name|. + if (feature_name.empty()) + return; + + std::string event_action = "InProductHelp.NotifyEvent."; + event_action.append(feature_name); + base::RecordComputedAction(event_action); + + std::string event_histogram = "InProductHelp.NotifyEventReadyState."; + event_histogram.append(feature_name); + base::UmaHistogramBoolean(event_histogram, is_model_ready); +} + +void RecordShouldTriggerHelpUI(const base::Feature& feature, + const ConditionValidator::Result& result) { + // Records the user action. + std::string name = std::string(kShouldTriggerHelpUIHistogram) + .append(".") + .append(feature.name); + base::RecordComputedAction(name); + + // Total count histogram, used to compute the percentage of each failure type, + // in addition to a user action for whether the result was to trigger or not. + if (result.NoErrors()) { + LogTriggerHelpUIResult(name, TriggerHelpUIResult::SUCCESS); + std::string name = "InProductHelp.ShouldTriggerHelpUIResult.Triggered."; + name.append(feature.name); + base::RecordComputedAction(name); + } else { + LogTriggerHelpUIResult(name, TriggerHelpUIResult::FAILURE); + std::string name = "InProductHelp.ShouldTriggerHelpUIResult.NotTriggered."; + name.append(feature.name); + base::RecordComputedAction(name); + } + + // Histogram about the failure reasons. + if (!result.event_model_ready_ok) { + LogTriggerHelpUIResult(name, + TriggerHelpUIResult::FAILURE_EVENT_MODEL_NOT_READY); + } + if (!result.currently_showing_ok) { + LogTriggerHelpUIResult(name, + TriggerHelpUIResult::FAILURE_CURRENTLY_SHOWING); + } + if (!result.feature_enabled_ok) { + LogTriggerHelpUIResult(name, TriggerHelpUIResult::FAILURE_FEATURE_DISABLED); + } + if (!result.config_ok) { + LogTriggerHelpUIResult(name, TriggerHelpUIResult::FAILURE_CONFIG_INVALID); + } + if (!result.used_ok) { + LogTriggerHelpUIResult( + name, TriggerHelpUIResult::FAILURE_USED_PRECONDITION_UNMET); + } + if (!result.trigger_ok) { + LogTriggerHelpUIResult( + name, TriggerHelpUIResult::FAILURE_TRIGGER_PRECONDITION_UNMET); + } + if (!result.preconditions_ok) { + LogTriggerHelpUIResult( + name, TriggerHelpUIResult::FAILURE_OTHER_PRECONDITION_UNMET); + } + if (!result.session_rate_ok) { + LogTriggerHelpUIResult(name, TriggerHelpUIResult::FAILURE_SESSION_RATE); + } + if (!result.availability_model_ready_ok) { + LogTriggerHelpUIResult( + name, TriggerHelpUIResult::FAILURE_AVAILABILITY_MODEL_NOT_READY); + } + if (!result.availability_ok) { + LogTriggerHelpUIResult( + name, TriggerHelpUIResult::FAILURE_AVAILABILITY_PRECONDITION_UNMET); + } +} + +void RecordUserDismiss() { + base::RecordAction(base::UserMetricsAction("InProductHelp.Dismissed")); +} + +void RecordDbUpdate(bool success, StoreType type) { + std::string histogram_name = + "InProductHelp.Db.Update." + ToDbHistogramSuffix(type); + base::UmaHistogramBoolean(histogram_name, success); +} + +void RecordDbInitEvent(bool success, StoreType type) { + std::string histogram_name = + "InProductHelp.Db.Init." + ToDbHistogramSuffix(type); + base::UmaHistogramBoolean(histogram_name, success); +} + +void RecordEventDbLoadEvent(bool success, const std::vector<Event>& events) { + std::string histogram_name = + "InProductHelp.Db.Load." + ToDbHistogramSuffix(StoreType::EVENTS_STORE); + base::UmaHistogramBoolean(histogram_name, success); + UMA_HISTOGRAM_BOOLEAN("InProductHelp.Db.Load", success); + + if (!success) + return; + + // Tracks total number of events records when the database is successfully + // loaded. + int event_count = 0; + for (const auto& event : events) + event_count += event.events_size(); + UMA_HISTOGRAM_COUNTS_1000("InProductHelp.Db.TotalEvents", event_count); +} + +void RecordAvailabilityDbLoadEvent(bool success) { + std::string histogram_name = + "InProductHelp.Db.Load." + + ToDbHistogramSuffix(StoreType::AVAILABILITY_STORE); + base::UmaHistogramBoolean(histogram_name, success); + UMA_HISTOGRAM_BOOLEAN("InProductHelp.Db.Load", success); +} + +void RecordConfigParsingEvent(ConfigParsingEvent event) { + UMA_HISTOGRAM_ENUMERATION("InProductHelp.Config.ParsingEvent", event, + ConfigParsingEvent::COUNT); +} + +} // namespace stats +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/stats.h b/chromium/components/feature_engagement/internal/stats.h new file mode 100644 index 00000000000..1a4022023ba --- /dev/null +++ b/chromium/components/feature_engagement/internal/stats.h @@ -0,0 +1,150 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_STATS_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_STATS_H_ + +#include <string> +#include <vector> + +#include "components/feature_engagement/internal/condition_validator.h" +#include "components/feature_engagement/internal/configuration.h" +#include "components/feature_engagement/internal/proto/event.pb.h" + +namespace feature_engagement { +namespace stats { + +// Enum used in the metrics to record the result when in-product help UI is +// going to be triggered. +// Most of the fields maps to |ConditionValidator::Result|. +// The failure reasons are not mutually exclusive. +// Out-dated entries shouldn't be deleted but marked as obselete. +enum class TriggerHelpUIResult { + // The help UI is triggered. + SUCCESS = 0, + + // The help UI is not triggered. + FAILURE = 1, + + // Event model is not ready. + FAILURE_EVENT_MODEL_NOT_READY = 2, + + // Some other help UI is currently showing. + FAILURE_CURRENTLY_SHOWING = 3, + + // The feature is disabled. + FAILURE_FEATURE_DISABLED = 4, + + // Configuration can not be parsed. + FAILURE_CONFIG_INVALID = 5, + + // Used event precondition is not satisfied. + FAILURE_USED_PRECONDITION_UNMET = 6, + + // Trigger event precondition is not satisfied. + FAILURE_TRIGGER_PRECONDITION_UNMET = 7, + + // Other event precondition is not satisfied. + FAILURE_OTHER_PRECONDITION_UNMET = 8, + + // Session rate does not meet the requirement. + FAILURE_SESSION_RATE = 9, + + // Availability model is not ready. + FAILURE_AVAILABILITY_MODEL_NOT_READY = 10, + + // Availability precondition is not satisfied. + FAILURE_AVAILABILITY_PRECONDITION_UNMET = 11, + + // Last entry for the enum. + COUNT = 12, +}; + +// Used in the metrics to track the configuration parsing event. +// The failure reasons are not mutually exclusive. +// Out-dated entries shouldn't be deleted but marked as obsolete. +enum class ConfigParsingEvent { + // The configuration is parsed correctly. + SUCCESS = 0, + + // The configuration is invalid after parsing. + FAILURE = 1, + + // Fails to parse the feature config because no field trial is found. + FAILURE_NO_FIELD_TRIAL = 2, + + // Fails to parse the used event. + FAILURE_USED_EVENT_PARSE = 3, + + // Used event is missing. + FAILURE_USED_EVENT_MISSING = 4, + + // Fails to parse the trigger event. + FAILURE_TRIGGER_EVENT_PARSE = 5, + + // Trigger event is missing. + FAILURE_TRIGGER_EVENT_MISSING = 6, + + // Fails to parse other events. + FAILURE_OTHER_EVENT_PARSE = 7, + + // Fails to parse the session rate comparator. + FAILURE_SESSION_RATE_PARSE = 8, + + // Fails to parse the availability comparator. + FAILURE_AVAILABILITY_PARSE = 9, + + // UnKnown key in configuration parameters. + FAILURE_UNKNOWN_KEY = 10, + + // Last entry for the enum. + COUNT = 11, +}; + +// Used in metrics to track database states. Each type will match to a suffix +// in the histograms to identify the database. +enum class StoreType { + // Events store. + EVENTS_STORE = 0, + + // Availability store. + AVAILABILITY_STORE = 1, +}; + +// Helper function that converts a store type to histogram suffix string. +std::string ToDbHistogramSuffix(StoreType type); + +// Records the feature engagement events. Used event will be tracked +// separately. +void RecordNotifyEvent(const std::string& event, + const Configuration* config, + bool is_model_ready); + +// Records user action and the result histogram when in-product help will be +// shown to the user. +void RecordShouldTriggerHelpUI(const base::Feature& feature, + const ConditionValidator::Result& result); + +// Records when the user dismisses the in-product help UI. +void RecordUserDismiss(); + +// Records the result of database updates. +void RecordDbUpdate(bool success, StoreType type); + +// Record database init. +void RecordDbInitEvent(bool success, StoreType type); + +// Records events database load event. +void RecordEventDbLoadEvent(bool success, const std::vector<Event>& events); + +// Records availability database load event. +void RecordAvailabilityDbLoadEvent(bool success); + +// Records configuration parsing event. +void RecordConfigParsingEvent(ConfigParsingEvent event); + +} // namespace stats +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_STATS_H_ diff --git a/chromium/components/feature_engagement/internal/system_time_provider.cc b/chromium/components/feature_engagement/internal/system_time_provider.cc new file mode 100644 index 00000000000..8e6d83d88ed --- /dev/null +++ b/chromium/components/feature_engagement/internal/system_time_provider.cc @@ -0,0 +1,24 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/system_time_provider.h" + +#include "base/time/time.h" + +namespace feature_engagement { + +SystemTimeProvider::SystemTimeProvider() = default; + +SystemTimeProvider::~SystemTimeProvider() = default; + +uint32_t SystemTimeProvider::GetCurrentDay() const { + base::TimeDelta delta = Now() - base::Time::UnixEpoch(); + return base::saturated_cast<uint32_t>(delta.InDays()); +} + +base::Time SystemTimeProvider::Now() const { + return base::Time::Now(); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/system_time_provider.h b/chromium/components/feature_engagement/internal/system_time_provider.h new file mode 100644 index 00000000000..bd48c687f21 --- /dev/null +++ b/chromium/components/feature_engagement/internal/system_time_provider.h @@ -0,0 +1,34 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_SYSTEM_TIME_PROVIDER_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_SYSTEM_TIME_PROVIDER_H_ + +#include "base/macros.h" +#include "base/time/time.h" +#include "components/feature_engagement/internal/time_provider.h" + +namespace feature_engagement { + +// A TimeProvider that uses the system time. +class SystemTimeProvider : public TimeProvider { + public: + SystemTimeProvider(); + ~SystemTimeProvider() override; + + // TimeProvider implementation. + uint32_t GetCurrentDay() const override; + + protected: + // Return the current time. + // virtual for testing. + virtual base::Time Now() const; + + private: + DISALLOW_COPY_AND_ASSIGN(SystemTimeProvider); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_SYSTEM_TIME_PROVIDER_H_ diff --git a/chromium/components/feature_engagement/internal/system_time_provider_unittest.cc b/chromium/components/feature_engagement/internal/system_time_provider_unittest.cc new file mode 100644 index 00000000000..d1ab4720d23 --- /dev/null +++ b/chromium/components/feature_engagement/internal/system_time_provider_unittest.cc @@ -0,0 +1,113 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/system_time_provider.h" + +#include "base/macros.h" +#include "base/time/time.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +namespace { + +base::Time GetTime(int year, int month, int day) { + base::Time::Exploded exploded_time; + exploded_time.year = year; + exploded_time.month = month; + exploded_time.day_of_month = day; + exploded_time.day_of_week = 0; + exploded_time.hour = 0; + exploded_time.minute = 0; + exploded_time.second = 0; + exploded_time.millisecond = 0; + + base::Time out_time; + EXPECT_TRUE(base::Time::FromUTCExploded(exploded_time, &out_time)); + return out_time; +} + +// A SystemTimeProvider where the current time can be defined at runtime. +class TestSystemTimeProvider : public SystemTimeProvider { + public: + TestSystemTimeProvider() = default; + + // SystemTimeProvider implementation. + base::Time Now() const override { return current_time_; } + + void SetCurrentTime(base::Time time) { current_time_ = time; } + + private: + base::Time current_time_; + + DISALLOW_COPY_AND_ASSIGN(TestSystemTimeProvider); +}; + +class SystemTimeProviderTest : public ::testing::Test { + public: + SystemTimeProviderTest() = default; + + protected: + TestSystemTimeProvider time_provider_; + + private: + DISALLOW_COPY_AND_ASSIGN(SystemTimeProviderTest); +}; + +} // namespace + +TEST_F(SystemTimeProviderTest, EpochIs0Days) { + time_provider_.SetCurrentTime(base::Time::UnixEpoch()); + EXPECT_EQ(0u, time_provider_.GetCurrentDay()); +} + +TEST_F(SystemTimeProviderTest, TestDeltasFromEpoch) { + base::Time epoch = base::Time::UnixEpoch(); + + time_provider_.SetCurrentTime(epoch + base::TimeDelta::FromDays(1)); + EXPECT_EQ(1u, time_provider_.GetCurrentDay()); + + time_provider_.SetCurrentTime(epoch + base::TimeDelta::FromDays(2)); + EXPECT_EQ(2u, time_provider_.GetCurrentDay()); + + time_provider_.SetCurrentTime(epoch + base::TimeDelta::FromDays(100)); + EXPECT_EQ(100u, time_provider_.GetCurrentDay()); +} + +TEST_F(SystemTimeProviderTest, TestNegativeDeltasFromEpoch) { + base::Time epoch = base::Time::UnixEpoch(); + + time_provider_.SetCurrentTime(epoch - base::TimeDelta::FromDays(1)); + EXPECT_EQ(0u, time_provider_.GetCurrentDay()); + + time_provider_.SetCurrentTime(epoch - base::TimeDelta::FromDays(2)); + EXPECT_EQ(0u, time_provider_.GetCurrentDay()); + + time_provider_.SetCurrentTime(epoch - base::TimeDelta::FromDays(100)); + EXPECT_EQ(0u, time_provider_.GetCurrentDay()); +} + +TEST_F(SystemTimeProviderTest, TestManualDatesAroundEpoch) { + time_provider_.SetCurrentTime(GetTime(1970, 1, 1)); + EXPECT_EQ(0u, time_provider_.GetCurrentDay()); + + time_provider_.SetCurrentTime(GetTime(1970, 1, 2)); + EXPECT_EQ(1u, time_provider_.GetCurrentDay()); + + time_provider_.SetCurrentTime(GetTime(1970, 4, 11)); + EXPECT_EQ(100u, time_provider_.GetCurrentDay()); +} + +TEST_F(SystemTimeProviderTest, TestManualDatesAroundGoogleIO2017) { + time_provider_.SetCurrentTime(GetTime(2017, 5, 17)); + EXPECT_EQ(17303u, time_provider_.GetCurrentDay()); + + time_provider_.SetCurrentTime(GetTime(2017, 5, 18)); + EXPECT_EQ(17304u, time_provider_.GetCurrentDay()); + + time_provider_.SetCurrentTime(GetTime(2017, 5, 19)); + EXPECT_EQ(17305u, time_provider_.GetCurrentDay()); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/test/BUILD.gn b/chromium/components/feature_engagement/internal/test/BUILD.gn new file mode 100644 index 00000000000..961c1f6066b --- /dev/null +++ b/chromium/components/feature_engagement/internal/test/BUILD.gn @@ -0,0 +1,19 @@ +# Copyright 2017 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +source_set("test_support") { + testonly = true + + visibility = [ "//components/feature_engagement/internal:unit_tests" ] + + sources = [ + "event_util.cc", + "event_util.h", + ] + + deps = [ + "//components/feature_engagement/internal/proto", + "//testing/gtest", + ] +} diff --git a/chromium/components/feature_engagement/internal/time_provider.h b/chromium/components/feature_engagement/internal/time_provider.h new file mode 100644 index 00000000000..50c73eeee69 --- /dev/null +++ b/chromium/components/feature_engagement/internal/time_provider.h @@ -0,0 +1,31 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_TIME_PROVIDER_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_TIME_PROVIDER_H_ + +#include <stdint.h> + +#include "base/macros.h" + +namespace feature_engagement { + +// A TimeProvider provides functionality related to time. +class TimeProvider { + public: + virtual ~TimeProvider() = default; + + // Returns the number of days since epoch (1970-01-01) in the local timezone. + virtual uint32_t GetCurrentDay() const = 0; + + protected: + TimeProvider() = default; + + private: + DISALLOW_COPY_AND_ASSIGN(TimeProvider); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_TIME_PROVIDER_H_ diff --git a/chromium/components/feature_engagement/internal/tracker_impl.cc b/chromium/components/feature_engagement/internal/tracker_impl.cc new file mode 100644 index 00000000000..8dc508a5caa --- /dev/null +++ b/chromium/components/feature_engagement/internal/tracker_impl.cc @@ -0,0 +1,270 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/tracker_impl.h" + +#include <memory> +#include <utility> + +#include "base/bind.h" +#include "base/feature_list.h" +#include "base/files/file_path.h" +#include "base/logging.h" +#include "base/memory/ptr_util.h" +#include "base/metrics/field_trial_params.h" +#include "base/metrics/user_metrics.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/feature_engagement/internal/availability_model_impl.h" +#include "components/feature_engagement/internal/chrome_variations_configuration.h" +#include "components/feature_engagement/internal/editable_configuration.h" +#include "components/feature_engagement/internal/event_model_impl.h" +#include "components/feature_engagement/internal/feature_config_condition_validator.h" +#include "components/feature_engagement/internal/feature_config_event_storage_validator.h" +#include "components/feature_engagement/internal/in_memory_event_store.h" +#include "components/feature_engagement/internal/init_aware_event_model.h" +#include "components/feature_engagement/internal/never_availability_model.h" +#include "components/feature_engagement/internal/never_event_storage_validator.h" +#include "components/feature_engagement/internal/once_condition_validator.h" +#include "components/feature_engagement/internal/persistent_event_store.h" +#include "components/feature_engagement/internal/proto/availability.pb.h" +#include "components/feature_engagement/internal/stats.h" +#include "components/feature_engagement/internal/system_time_provider.h" +#include "components/feature_engagement/public/feature_constants.h" +#include "components/feature_engagement/public/feature_list.h" +#include "components/leveldb_proto/proto_database_impl.h" + +namespace feature_engagement { + +namespace { +const base::FilePath::CharType kEventDBStorageDir[] = + FILE_PATH_LITERAL("EventDB"); +const base::FilePath::CharType kAvailabilityDBStorageDir[] = + FILE_PATH_LITERAL("AvailabilityDB"); + +// Creates a TrackerImpl that is usable for a demo mode. +std::unique_ptr<Tracker> CreateDemoModeTracker() { + // GetFieldTrialParamValueByFeature returns an empty string if the param is + // not set. + std::string chosen_feature_name = base::GetFieldTrialParamValueByFeature( + kIPHDemoMode, kIPHDemoModeFeatureChoiceParam); + + DVLOG(2) << "Enabling demo mode. Chosen feature: " << chosen_feature_name; + + std::unique_ptr<EditableConfiguration> configuration = + base::MakeUnique<EditableConfiguration>(); + + // Create valid configurations for all features to ensure that the + // OnceConditionValidator acknowledges that thet meet conditions once. + std::vector<const base::Feature*> features = GetAllFeatures(); + for (auto* feature : features) { + // If a particular feature has been chosen to use with demo mode, only + // mark that feature with a valid configuration. + bool valid_config = chosen_feature_name.empty() + ? true + : chosen_feature_name == feature->name; + + FeatureConfig feature_config; + feature_config.valid = valid_config; + feature_config.trigger.name = feature->name + std::string("_trigger"); + configuration->SetConfiguration(feature, feature_config); + } + + auto raw_event_model = base::MakeUnique<EventModelImpl>( + base::MakeUnique<InMemoryEventStore>(), + base::MakeUnique<NeverEventStorageValidator>()); + + return base::MakeUnique<TrackerImpl>( + base::MakeUnique<InitAwareEventModel>(std::move(raw_event_model)), + base::MakeUnique<NeverAvailabilityModel>(), std::move(configuration), + base::MakeUnique<OnceConditionValidator>(), + base::MakeUnique<SystemTimeProvider>()); +} + +} // namespace + +// This method is declared in //components/feature_engagement/public/ +// feature_engagement.h +// and should be linked in to any binary using Tracker::Create. +// static +Tracker* Tracker::Create( + const base::FilePath& storage_dir, + const scoped_refptr<base::SequencedTaskRunner>& background_task_runner) { + DVLOG(2) << "Creating Tracker"; + if (base::FeatureList::IsEnabled(kIPHDemoMode)) + return CreateDemoModeTracker().release(); + + std::unique_ptr<leveldb_proto::ProtoDatabase<Event>> event_db = + base::MakeUnique<leveldb_proto::ProtoDatabaseImpl<Event>>( + background_task_runner); + + base::FilePath event_storage_dir = storage_dir.Append(kEventDBStorageDir); + auto event_store = base::MakeUnique<PersistentEventStore>( + event_storage_dir, std::move(event_db)); + + auto configuration = base::MakeUnique<ChromeVariationsConfiguration>(); + configuration->ParseFeatureConfigs(GetAllFeatures()); + + auto event_storage_validator = + base::MakeUnique<FeatureConfigEventStorageValidator>(); + event_storage_validator->InitializeFeatures(GetAllFeatures(), *configuration); + + auto raw_event_model = base::MakeUnique<EventModelImpl>( + std::move(event_store), std::move(event_storage_validator)); + + auto event_model = + base::MakeUnique<InitAwareEventModel>(std::move(raw_event_model)); + auto condition_validator = + base::MakeUnique<FeatureConfigConditionValidator>(); + auto time_provider = base::MakeUnique<SystemTimeProvider>(); + + base::FilePath availability_storage_dir = + storage_dir.Append(kAvailabilityDBStorageDir); + auto availability_db = + base::MakeUnique<leveldb_proto::ProtoDatabaseImpl<Availability>>( + background_task_runner); + auto availability_store_loader = base::BindOnce( + &PersistentAvailabilityStore::LoadAndUpdateStore, + availability_storage_dir, std::move(availability_db), GetAllFeatures()); + + auto availability_model = base::MakeUnique<AvailabilityModelImpl>( + std::move(availability_store_loader)); + + return new TrackerImpl(std::move(event_model), std::move(availability_model), + std::move(configuration), + std::move(condition_validator), + std::move(time_provider)); +} + +TrackerImpl::TrackerImpl( + std::unique_ptr<EventModel> event_model, + std::unique_ptr<AvailabilityModel> availability_model, + std::unique_ptr<Configuration> configuration, + std::unique_ptr<ConditionValidator> condition_validator, + std::unique_ptr<TimeProvider> time_provider) + : event_model_(std::move(event_model)), + availability_model_(std::move(availability_model)), + configuration_(std::move(configuration)), + condition_validator_(std::move(condition_validator)), + time_provider_(std::move(time_provider)), + event_model_initialization_finished_(false), + availability_model_initialization_finished_(false), + weak_ptr_factory_(this) { + event_model_->Initialize( + base::Bind(&TrackerImpl::OnEventModelInitializationFinished, + weak_ptr_factory_.GetWeakPtr()), + time_provider_->GetCurrentDay()); + + availability_model_->Initialize( + base::Bind(&TrackerImpl::OnAvailabilityModelInitializationFinished, + weak_ptr_factory_.GetWeakPtr()), + time_provider_->GetCurrentDay()); +} + +TrackerImpl::~TrackerImpl() = default; + +void TrackerImpl::NotifyEvent(const std::string& event) { + event_model_->IncrementEvent(event, time_provider_->GetCurrentDay()); + stats::RecordNotifyEvent(event, configuration_.get(), + event_model_->IsReady()); +} + +bool TrackerImpl::ShouldTriggerHelpUI(const base::Feature& feature) { + ConditionValidator::Result result = condition_validator_->MeetsConditions( + feature, configuration_->GetFeatureConfig(feature), *event_model_, + *availability_model_, time_provider_->GetCurrentDay()); + if (result.NoErrors()) { + condition_validator_->NotifyIsShowing(feature); + FeatureConfig feature_config = configuration_->GetFeatureConfig(feature); + DCHECK_NE("", feature_config.trigger.name); + event_model_->IncrementEvent(feature_config.trigger.name, + time_provider_->GetCurrentDay()); + } + + stats::RecordShouldTriggerHelpUI(feature, result); + DVLOG(2) << "Trigger result for " << feature.name + << ": trigger=" << result.NoErrors() << " " << result; + return result.NoErrors(); +} + +Tracker::TriggerState TrackerImpl::GetTriggerState( + const base::Feature& feature) { + if (!IsInitialized()) { + DVLOG(2) << "TriggerState for " << feature.name << ": " + << static_cast<int>(Tracker::TriggerState::NOT_READY); + return Tracker::TriggerState::NOT_READY; + } + + ConditionValidator::Result result = condition_validator_->MeetsConditions( + feature, configuration_->GetFeatureConfig(feature), *event_model_, + *availability_model_, time_provider_->GetCurrentDay()); + + if (result.trigger_ok) { + DVLOG(2) << "TriggerState for " << feature.name << ": " + << static_cast<int>(Tracker::TriggerState::HAS_NOT_BEEN_DISPLAYED); + return Tracker::TriggerState::HAS_NOT_BEEN_DISPLAYED; + } + + DVLOG(2) << "TriggerState for " << feature.name << ": " + << static_cast<int>(Tracker::TriggerState::HAS_BEEN_DISPLAYED); + return Tracker::TriggerState::HAS_BEEN_DISPLAYED; +} + +void TrackerImpl::Dismissed(const base::Feature& feature) { + DVLOG(2) << "Dismissing " << feature.name; + condition_validator_->NotifyDismissed(feature); + stats::RecordUserDismiss(); +} + +bool TrackerImpl::IsInitialized() { + return event_model_->IsReady() && availability_model_->IsReady(); +} + +void TrackerImpl::AddOnInitializedCallback(OnInitializedCallback callback) { + if (IsInitializationFinished()) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, IsInitialized())); + return; + } + + on_initialized_callbacks_.push_back(callback); +} + +void TrackerImpl::OnEventModelInitializationFinished(bool success) { + DCHECK_EQ(success, event_model_->IsReady()); + event_model_initialization_finished_ = true; + + DVLOG(2) << "Event model initialization result = " << success; + + MaybePostInitializedCallbacks(); +} + +void TrackerImpl::OnAvailabilityModelInitializationFinished(bool success) { + DCHECK_EQ(success, availability_model_->IsReady()); + availability_model_initialization_finished_ = true; + + DVLOG(2) << "Availability model initialization result = " << success; + + MaybePostInitializedCallbacks(); +} + +bool TrackerImpl::IsInitializationFinished() const { + return event_model_initialization_finished_ && + availability_model_initialization_finished_; +} + +void TrackerImpl::MaybePostInitializedCallbacks() { + if (!IsInitializationFinished()) + return; + + DVLOG(2) << "Initialization finished."; + + for (auto& callback : on_initialized_callbacks_) { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, IsInitialized())); + } + + on_initialized_callbacks_.clear(); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/internal/tracker_impl.h b/chromium/components/feature_engagement/internal/tracker_impl.h new file mode 100644 index 00000000000..c9d37023601 --- /dev/null +++ b/chromium/components/feature_engagement/internal/tracker_impl.h @@ -0,0 +1,92 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_TRACKER_IMPL_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_TRACKER_IMPL_H_ + +#include <string> +#include <vector> + +#include "base/feature_list.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/supports_user_data.h" +#include "components/feature_engagement/public/tracker.h" + +namespace feature_engagement { +class AvailabilityModel; +class Configuration; +class ConditionValidator; +class EventModel; +class TimeProvider; + +// The internal implementation of the Tracker. +class TrackerImpl : public Tracker, public base::SupportsUserData { + public: + TrackerImpl(std::unique_ptr<EventModel> event_model, + std::unique_ptr<AvailabilityModel> availability_model, + std::unique_ptr<Configuration> configuration, + std::unique_ptr<ConditionValidator> condition_validator, + std::unique_ptr<TimeProvider> time_provider); + ~TrackerImpl() override; + + // Tracker implementation. + void NotifyEvent(const std::string& event) override; + bool ShouldTriggerHelpUI(const base::Feature& feature) override; + Tracker::TriggerState GetTriggerState(const base::Feature& feature) override; + void Dismissed(const base::Feature& feature) override; + bool IsInitialized() override; + void AddOnInitializedCallback(OnInitializedCallback callback) override; + + private: + // Invoked by the EventModel when it has been initialized. + void OnEventModelInitializationFinished(bool success); + + // Invoked by the AvailabilityModel when it has been initialized. + void OnAvailabilityModelInitializationFinished(bool success); + + // Returns whether both underlying models have finished initializing. + // This returning true does not mean the initialization was a success, just + // that it is finished. + bool IsInitializationFinished() const; + + // Posts the results to the OnInitializedCallbacks if + // IsInitializationFinished() returns true. + void MaybePostInitializedCallbacks(); + + // The current model for all events. + std::unique_ptr<EventModel> event_model_; + + // The current model for when particular features were enabled. + std::unique_ptr<AvailabilityModel> availability_model_; + + // The current configuration for all features. + std::unique_ptr<Configuration> configuration_; + + // The ConditionValidator provides functionality for knowing when to trigger + // help UI. + std::unique_ptr<ConditionValidator> condition_validator_; + + // A utility for retriving time-related information. + std::unique_ptr<TimeProvider> time_provider_; + + // Whether the initialization of the underlying EventModel has finished. + bool event_model_initialization_finished_; + + // Whether the initialization of the underlying AvailabilityModel has + // finished. + bool availability_model_initialization_finished_; + + // The list of callbacks to invoke when initialization has finished. This + // is cleared after the initialization has happened. + std::vector<OnInitializedCallback> on_initialized_callbacks_; + + base::WeakPtrFactory<TrackerImpl> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(TrackerImpl); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_TRACKER_IMPL_H_ diff --git a/chromium/components/feature_engagement/internal/tracker_impl_unittest.cc b/chromium/components/feature_engagement/internal/tracker_impl_unittest.cc new file mode 100644 index 00000000000..078059b7017 --- /dev/null +++ b/chromium/components/feature_engagement/internal/tracker_impl_unittest.cc @@ -0,0 +1,649 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/internal/tracker_impl.h" + +#include <memory> + +#include "base/bind.h" +#include "base/feature_list.h" +#include "base/memory/ptr_util.h" +#include "base/message_loop/message_loop.h" +#include "base/run_loop.h" +#include "base/sequenced_task_runner.h" +#include "base/single_thread_task_runner.h" +#include "base/test/histogram_tester.h" +#include "base/test/user_action_tester.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/feature_engagement/internal/availability_model_impl.h" +#include "components/feature_engagement/internal/editable_configuration.h" +#include "components/feature_engagement/internal/event_model_impl.h" +#include "components/feature_engagement/internal/in_memory_event_store.h" +#include "components/feature_engagement/internal/never_availability_model.h" +#include "components/feature_engagement/internal/never_event_storage_validator.h" +#include "components/feature_engagement/internal/once_condition_validator.h" +#include "components/feature_engagement/internal/stats.h" +#include "components/feature_engagement/internal/time_provider.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace feature_engagement { + +namespace { +const base::Feature kTestFeatureFoo{"test_foo", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureBar{"test_bar", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kTestFeatureQux{"test_qux", + base::FEATURE_DISABLED_BY_DEFAULT}; + +void RegisterFeatureConfig(EditableConfiguration* configuration, + const base::Feature& feature, + bool valid) { + FeatureConfig config; + config.valid = valid; + config.used.name = feature.name + std::string("_used"); + config.trigger.name = feature.name + std::string("_trigger"); + configuration->SetConfiguration(&feature, config); +} + +// An OnInitializedCallback that stores whether it has been invoked and what +// the result was. +class StoringInitializedCallback { + public: + StoringInitializedCallback() : invoked_(false), success_(false) {} + + void OnInitialized(bool success) { + DCHECK(!invoked_); + invoked_ = true; + success_ = success; + } + + bool invoked() { return invoked_; } + + bool success() { return success_; } + + private: + bool invoked_; + bool success_; + + DISALLOW_COPY_AND_ASSIGN(StoringInitializedCallback); +}; + +// An InMemoryEventStore that is able to fake successful and unsuccessful +// loading of state. +class TestInMemoryEventStore : public InMemoryEventStore { + public: + explicit TestInMemoryEventStore(bool load_should_succeed) + : InMemoryEventStore(), load_should_succeed_(load_should_succeed) {} + + void Load(const OnLoadedCallback& callback) override { + HandleLoadResult(callback, load_should_succeed_); + } + + void WriteEvent(const Event& event) override { + events_[event.name()] = event; + } + + Event GetEvent(const std::string& event_name) { return events_[event_name]; } + + private: + // Denotes whether the call to Load(...) should succeed or not. This impacts + // both the ready-state and the result for the OnLoadedCallback. + bool load_should_succeed_; + + std::map<std::string, Event> events_; + + DISALLOW_COPY_AND_ASSIGN(TestInMemoryEventStore); +}; + +class StoreEverythingEventStorageValidator : public EventStorageValidator { + public: + StoreEverythingEventStorageValidator() = default; + ~StoreEverythingEventStorageValidator() override = default; + + bool ShouldStore(const std::string& event_name) const override { + return true; + } + + bool ShouldKeep(const std::string& event_name, + uint32_t event_day, + uint32_t current_day) const override { + return true; + }; + + private: + DISALLOW_COPY_AND_ASSIGN(StoreEverythingEventStorageValidator); +}; + +class TestTimeProvider : public TimeProvider { + public: + TestTimeProvider() = default; + ~TestTimeProvider() override = default; + + // TimeProvider implementation. + uint32_t GetCurrentDay() const override { return 1u; }; + + private: + DISALLOW_COPY_AND_ASSIGN(TestTimeProvider); +}; + +class TestAvailabilityModel : public AvailabilityModel { + public: + TestAvailabilityModel() : ready_(true) {} + ~TestAvailabilityModel() override = default; + + void Initialize(AvailabilityModel::OnInitializedCallback callback, + uint32_t current_day) override { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), ready_)); + } + + bool IsReady() const override { return ready_; } + + void SetIsReady(bool ready) { ready_ = ready; } + + base::Optional<uint32_t> GetAvailability( + const base::Feature& feature) const override { + return base::nullopt; + } + + private: + bool ready_; + + DISALLOW_COPY_AND_ASSIGN(TestAvailabilityModel); +}; + +class TrackerImplTest : public ::testing::Test { + public: + TrackerImplTest() = default; + + void SetUp() override { + std::unique_ptr<EditableConfiguration> configuration = + base::MakeUnique<EditableConfiguration>(); + configuration_ = configuration.get(); + + RegisterFeatureConfig(configuration.get(), kTestFeatureFoo, true); + RegisterFeatureConfig(configuration.get(), kTestFeatureBar, true); + RegisterFeatureConfig(configuration.get(), kTestFeatureQux, false); + + std::unique_ptr<TestInMemoryEventStore> event_store = CreateEventStore(); + event_store_ = event_store.get(); + + auto event_model = base::MakeUnique<EventModelImpl>( + std::move(event_store), + base::MakeUnique<StoreEverythingEventStorageValidator>()); + + auto availability_model = base::MakeUnique<TestAvailabilityModel>(); + availability_model_ = availability_model.get(); + availability_model_->SetIsReady(ShouldAvailabilityStoreBeReady()); + + tracker_.reset(new TrackerImpl( + std::move(event_model), std::move(availability_model), + std::move(configuration), base::MakeUnique<OnceConditionValidator>(), + base::MakeUnique<TestTimeProvider>())); + } + + void VerifyEventTriggerEvents(const base::Feature& feature, uint32_t count) { + Event trigger_event = event_store_->GetEvent( + configuration_->GetFeatureConfig(feature).trigger.name); + if (count == 0) { + EXPECT_EQ(0, trigger_event.events_size()); + return; + } + + EXPECT_EQ(1, trigger_event.events_size()); + EXPECT_EQ(1u, trigger_event.events(0).day()); + EXPECT_EQ(count, trigger_event.events(0).count()); + } + + void VerifyHistogramsForFeature(const std::string& histogram_name, + bool check, + int expected_success_count, + int expected_failure_count) { + if (!check) + return; + + histogram_tester_.ExpectBucketCount( + histogram_name, static_cast<int>(stats::TriggerHelpUIResult::SUCCESS), + expected_success_count); + histogram_tester_.ExpectBucketCount( + histogram_name, static_cast<int>(stats::TriggerHelpUIResult::FAILURE), + expected_failure_count); + } + + // Histogram values are checked only if their respective |check_...| is true, + // since inspecting a bucket count for a histogram that has not been recorded + // yet leads to an error. + void VerifyHistograms(bool check_foo, + int expected_foo_success_count, + int expected_foo_failure_count, + bool check_bar, + int expected_bar_success_count, + int expected_bar_failure_count, + bool check_qux, + int expected_qux_success_count, + int expected_qux_failure_count) { + VerifyHistogramsForFeature("InProductHelp.ShouldTriggerHelpUI.test_foo", + check_foo, expected_foo_success_count, + expected_foo_failure_count); + VerifyHistogramsForFeature("InProductHelp.ShouldTriggerHelpUI.test_bar", + check_bar, expected_bar_success_count, + expected_bar_failure_count); + VerifyHistogramsForFeature("InProductHelp.ShouldTriggerHelpUI.test_qux", + check_qux, expected_qux_success_count, + expected_qux_failure_count); + + int expected_total_successes = expected_foo_success_count + + expected_bar_success_count + + expected_qux_success_count; + int expected_total_failures = expected_foo_failure_count + + expected_bar_failure_count + + expected_qux_failure_count; + VerifyHistogramsForFeature("InProductHelp.ShouldTriggerHelpUI", true, + expected_total_successes, + expected_total_failures); + } + + void VerifyUserActionsTriggerChecks( + const base::UserActionTester& user_action_tester, + int expected_foo_count, + int expected_bar_count, + int expected_qux_count) { + EXPECT_EQ(expected_foo_count, + user_action_tester.GetActionCount( + "InProductHelp.ShouldTriggerHelpUI.test_foo")); + EXPECT_EQ(expected_bar_count, + user_action_tester.GetActionCount( + "InProductHelp.ShouldTriggerHelpUI.test_bar")); + EXPECT_EQ(expected_qux_count, + user_action_tester.GetActionCount( + "InProductHelp.ShouldTriggerHelpUI.test_qux")); + } + + void VerifyUserActionsTriggered( + const base::UserActionTester& user_action_tester, + int expected_foo_count, + int expected_bar_count, + int expected_qux_count) { + EXPECT_EQ( + expected_foo_count, + user_action_tester.GetActionCount( + "InProductHelp.ShouldTriggerHelpUIResult.Triggered.test_foo")); + EXPECT_EQ( + expected_bar_count, + user_action_tester.GetActionCount( + "InProductHelp.ShouldTriggerHelpUIResult.Triggered.test_bar")); + EXPECT_EQ( + expected_qux_count, + user_action_tester.GetActionCount( + "InProductHelp.ShouldTriggerHelpUIResult.Triggered.test_qux")); + } + + void VerifyUserActionsNotTriggered( + const base::UserActionTester& user_action_tester, + int expected_foo_count, + int expected_bar_count, + int expected_qux_count) { + EXPECT_EQ( + expected_foo_count, + user_action_tester.GetActionCount( + "InProductHelp.ShouldTriggerHelpUIResult.NotTriggered.test_foo")); + EXPECT_EQ( + expected_bar_count, + user_action_tester.GetActionCount( + "InProductHelp.ShouldTriggerHelpUIResult.NotTriggered.test_bar")); + EXPECT_EQ( + expected_qux_count, + user_action_tester.GetActionCount( + "InProductHelp.ShouldTriggerHelpUIResult.NotTriggered.test_qux")); + } + + void VerifyUserActionsDismissed( + const base::UserActionTester& user_action_tester, + int expected_dismissed_count) { + EXPECT_EQ(expected_dismissed_count, + user_action_tester.GetActionCount("InProductHelp.Dismissed")); + } + + protected: + virtual std::unique_ptr<TestInMemoryEventStore> CreateEventStore() { + // Returns a EventStore that will successfully initialize. + return base::MakeUnique<TestInMemoryEventStore>(true); + } + + virtual bool ShouldAvailabilityStoreBeReady() { return true; } + + base::MessageLoop message_loop_; + std::unique_ptr<TrackerImpl> tracker_; + TestInMemoryEventStore* event_store_; + TestAvailabilityModel* availability_model_; + Configuration* configuration_; + base::HistogramTester histogram_tester_; + + private: + DISALLOW_COPY_AND_ASSIGN(TrackerImplTest); +}; + +// A top-level test class where the store fails to initialize. +class FailingStoreInitTrackerImplTest : public TrackerImplTest { + public: + FailingStoreInitTrackerImplTest() = default; + + protected: + std::unique_ptr<TestInMemoryEventStore> CreateEventStore() override { + // Returns a EventStore that will fail to initialize. + return base::MakeUnique<TestInMemoryEventStore>(false); + } + + private: + DISALLOW_COPY_AND_ASSIGN(FailingStoreInitTrackerImplTest); +}; + +// A top-level test class where the AvailabilityModel fails to initialize. +class FailingAvailabilityModelInitTrackerImplTest : public TrackerImplTest { + public: + FailingAvailabilityModelInitTrackerImplTest() = default; + + protected: + bool ShouldAvailabilityStoreBeReady() override { return false; } + + private: + DISALLOW_COPY_AND_ASSIGN(FailingAvailabilityModelInitTrackerImplTest); +}; + +} // namespace + +TEST_F(TrackerImplTest, TestInitialization) { + EXPECT_FALSE(tracker_->IsInitialized()); + + StoringInitializedCallback callback; + tracker_->AddOnInitializedCallback(base::Bind( + &StoringInitializedCallback::OnInitialized, base::Unretained(&callback))); + EXPECT_FALSE(callback.invoked()); + + // Ensure all initialization is finished. + base::RunLoop().RunUntilIdle(); + + EXPECT_TRUE(tracker_->IsInitialized()); + EXPECT_TRUE(callback.invoked()); + EXPECT_TRUE(callback.success()); +} + +TEST_F(TrackerImplTest, TestInitializationMultipleCallbacks) { + EXPECT_FALSE(tracker_->IsInitialized()); + + StoringInitializedCallback callback1; + StoringInitializedCallback callback2; + + tracker_->AddOnInitializedCallback( + base::Bind(&StoringInitializedCallback::OnInitialized, + base::Unretained(&callback1))); + tracker_->AddOnInitializedCallback( + base::Bind(&StoringInitializedCallback::OnInitialized, + base::Unretained(&callback2))); + EXPECT_FALSE(callback1.invoked()); + EXPECT_FALSE(callback2.invoked()); + + // Ensure all initialization is finished. + base::RunLoop().RunUntilIdle(); + + EXPECT_TRUE(tracker_->IsInitialized()); + EXPECT_TRUE(callback1.invoked()); + EXPECT_TRUE(callback2.invoked()); + EXPECT_TRUE(callback1.success()); + EXPECT_TRUE(callback2.success()); +} + +TEST_F(TrackerImplTest, TestAddingCallbackAfterInitFinished) { + EXPECT_FALSE(tracker_->IsInitialized()); + + // Ensure all initialization is finished. + base::RunLoop().RunUntilIdle(); + + EXPECT_TRUE(tracker_->IsInitialized()); + + StoringInitializedCallback callback; + tracker_->AddOnInitializedCallback(base::Bind( + &StoringInitializedCallback::OnInitialized, base::Unretained(&callback))); + EXPECT_FALSE(callback.invoked()); + + base::RunLoop().RunUntilIdle(); + + EXPECT_TRUE(callback.invoked()); +} + +TEST_F(TrackerImplTest, TestAddingCallbackBeforeAndAfterInitFinished) { + EXPECT_FALSE(tracker_->IsInitialized()); + + // Ensure all initialization is finished. + base::RunLoop().RunUntilIdle(); + + EXPECT_TRUE(tracker_->IsInitialized()); + + StoringInitializedCallback callback_before; + tracker_->AddOnInitializedCallback( + base::Bind(&StoringInitializedCallback::OnInitialized, + base::Unretained(&callback_before))); + EXPECT_FALSE(callback_before.invoked()); + + base::RunLoop().RunUntilIdle(); + + EXPECT_TRUE(callback_before.invoked()); + + StoringInitializedCallback callback_after; + tracker_->AddOnInitializedCallback( + base::Bind(&StoringInitializedCallback::OnInitialized, + base::Unretained(&callback_after))); + EXPECT_FALSE(callback_after.invoked()); + + base::RunLoop().RunUntilIdle(); + + EXPECT_TRUE(callback_after.invoked()); +} + +TEST_F(FailingStoreInitTrackerImplTest, TestFailingInitialization) { + EXPECT_FALSE(tracker_->IsInitialized()); + + StoringInitializedCallback callback; + tracker_->AddOnInitializedCallback(base::Bind( + &StoringInitializedCallback::OnInitialized, base::Unretained(&callback))); + EXPECT_FALSE(callback.invoked()); + + // Ensure all initialization is finished. + base::RunLoop().RunUntilIdle(); + + EXPECT_FALSE(tracker_->IsInitialized()); + EXPECT_TRUE(callback.invoked()); + EXPECT_FALSE(callback.success()); +} + +TEST_F(FailingStoreInitTrackerImplTest, + TestFailingInitializationMultipleCallbacks) { + EXPECT_FALSE(tracker_->IsInitialized()); + + StoringInitializedCallback callback1; + StoringInitializedCallback callback2; + tracker_->AddOnInitializedCallback( + base::Bind(&StoringInitializedCallback::OnInitialized, + base::Unretained(&callback1))); + tracker_->AddOnInitializedCallback( + base::Bind(&StoringInitializedCallback::OnInitialized, + base::Unretained(&callback2))); + EXPECT_FALSE(callback1.invoked()); + EXPECT_FALSE(callback2.invoked()); + + // Ensure all initialization is finished. + base::RunLoop().RunUntilIdle(); + + EXPECT_FALSE(tracker_->IsInitialized()); + EXPECT_TRUE(callback1.invoked()); + EXPECT_TRUE(callback2.invoked()); + EXPECT_FALSE(callback1.success()); + EXPECT_FALSE(callback2.success()); +} + +TEST_F(FailingAvailabilityModelInitTrackerImplTest, AvailabilityModelNotReady) { + EXPECT_FALSE(tracker_->IsInitialized()); + + StoringInitializedCallback callback; + tracker_->AddOnInitializedCallback(base::Bind( + &StoringInitializedCallback::OnInitialized, base::Unretained(&callback))); + EXPECT_FALSE(callback.invoked()); + + // Ensure all initialization is finished. + base::RunLoop().RunUntilIdle(); + + EXPECT_FALSE(tracker_->IsInitialized()); + EXPECT_TRUE(callback.invoked()); + EXPECT_FALSE(callback.success()); +} + +TEST_F(TrackerImplTest, TestTriggering) { + // Ensure all initialization is finished. + StoringInitializedCallback callback; + tracker_->AddOnInitializedCallback(base::Bind( + &StoringInitializedCallback::OnInitialized, base::Unretained(&callback))); + base::RunLoop().RunUntilIdle(); + base::UserActionTester user_action_tester; + + // The first time a feature triggers it should be shown. + EXPECT_TRUE(tracker_->ShouldTriggerHelpUI(kTestFeatureFoo)); + VerifyEventTriggerEvents(kTestFeatureFoo, 1u); + EXPECT_FALSE(tracker_->ShouldTriggerHelpUI(kTestFeatureFoo)); + VerifyEventTriggerEvents(kTestFeatureFoo, 1u); + EXPECT_FALSE(tracker_->ShouldTriggerHelpUI(kTestFeatureQux)); + VerifyEventTriggerEvents(kTestFeatureQux, 0); + VerifyUserActionsTriggerChecks(user_action_tester, 2, 0, 1); + VerifyUserActionsTriggered(user_action_tester, 1, 0, 0); + VerifyUserActionsNotTriggered(user_action_tester, 1, 0, 1); + VerifyUserActionsDismissed(user_action_tester, 0); + VerifyHistograms(true, 1, 1, false, 0, 0, true, 0, 1); + + // While in-product help is currently showing, no other features should be + // shown. + EXPECT_FALSE(tracker_->ShouldTriggerHelpUI(kTestFeatureBar)); + VerifyEventTriggerEvents(kTestFeatureBar, 0); + EXPECT_FALSE(tracker_->ShouldTriggerHelpUI(kTestFeatureQux)); + VerifyEventTriggerEvents(kTestFeatureQux, 0); + VerifyUserActionsTriggerChecks(user_action_tester, 2, 1, 2); + VerifyUserActionsTriggered(user_action_tester, 1, 0, 0); + VerifyUserActionsNotTriggered(user_action_tester, 1, 1, 2); + VerifyUserActionsDismissed(user_action_tester, 0); + VerifyHistograms(true, 1, 1, true, 0, 1, true, 0, 2); + + // After dismissing the current in-product help, that feature can not be shown + // again, but a different feature should. + tracker_->Dismissed(kTestFeatureFoo); + EXPECT_FALSE(tracker_->ShouldTriggerHelpUI(kTestFeatureFoo)); + VerifyEventTriggerEvents(kTestFeatureFoo, 1u); + EXPECT_TRUE(tracker_->ShouldTriggerHelpUI(kTestFeatureBar)); + VerifyEventTriggerEvents(kTestFeatureBar, 1u); + EXPECT_FALSE(tracker_->ShouldTriggerHelpUI(kTestFeatureQux)); + VerifyEventTriggerEvents(kTestFeatureQux, 0); + VerifyUserActionsTriggerChecks(user_action_tester, 3, 2, 3); + VerifyUserActionsTriggered(user_action_tester, 1, 1, 0); + VerifyUserActionsNotTriggered(user_action_tester, 2, 1, 3); + VerifyUserActionsDismissed(user_action_tester, 1); + VerifyHistograms(true, 1, 2, true, 1, 1, true, 0, 3); + + // After dismissing the second registered feature, no more in-product help + // should be shown, since kTestFeatureQux is invalid. + tracker_->Dismissed(kTestFeatureBar); + EXPECT_FALSE(tracker_->ShouldTriggerHelpUI(kTestFeatureFoo)); + VerifyEventTriggerEvents(kTestFeatureFoo, 1u); + EXPECT_FALSE(tracker_->ShouldTriggerHelpUI(kTestFeatureBar)); + VerifyEventTriggerEvents(kTestFeatureBar, 1u); + EXPECT_FALSE(tracker_->ShouldTriggerHelpUI(kTestFeatureQux)); + VerifyEventTriggerEvents(kTestFeatureQux, 0); + VerifyUserActionsTriggerChecks(user_action_tester, 4, 3, 4); + VerifyUserActionsTriggered(user_action_tester, 1, 1, 0); + VerifyUserActionsNotTriggered(user_action_tester, 3, 2, 4); + VerifyUserActionsDismissed(user_action_tester, 2); + VerifyHistograms(true, 1, 3, true, 1, 2, true, 0, 4); +} + +TEST_F(TrackerImplTest, TestTriggerStateInspection) { + // Before initialization has finished, NOT_READY should always be returned. + EXPECT_EQ(Tracker::TriggerState::NOT_READY, + tracker_->GetTriggerState(kTestFeatureFoo)); + EXPECT_EQ(Tracker::TriggerState::NOT_READY, + tracker_->GetTriggerState(kTestFeatureQux)); + + // Ensure all initialization is finished. + StoringInitializedCallback callback; + tracker_->AddOnInitializedCallback(base::Bind( + &StoringInitializedCallback::OnInitialized, base::Unretained(&callback))); + base::RunLoop().RunUntilIdle(); + base::UserActionTester user_action_tester; + + EXPECT_EQ(Tracker::TriggerState::HAS_NOT_BEEN_DISPLAYED, + tracker_->GetTriggerState(kTestFeatureFoo)); + EXPECT_EQ(Tracker::TriggerState::HAS_NOT_BEEN_DISPLAYED, + tracker_->GetTriggerState(kTestFeatureBar)); + + // The first time a feature triggers it should be shown. + EXPECT_TRUE(tracker_->ShouldTriggerHelpUI(kTestFeatureFoo)); + VerifyEventTriggerEvents(kTestFeatureFoo, 1u); + EXPECT_EQ(Tracker::TriggerState::HAS_BEEN_DISPLAYED, + tracker_->GetTriggerState(kTestFeatureFoo)); + + // Trying to show again should keep state as displayed. + EXPECT_FALSE(tracker_->ShouldTriggerHelpUI(kTestFeatureFoo)); + VerifyEventTriggerEvents(kTestFeatureFoo, 1u); + EXPECT_EQ(Tracker::TriggerState::HAS_BEEN_DISPLAYED, + tracker_->GetTriggerState(kTestFeatureFoo)); + + // Other features should also be kept at not having been displayed. + EXPECT_FALSE(tracker_->ShouldTriggerHelpUI(kTestFeatureBar)); + VerifyEventTriggerEvents(kTestFeatureBar, 0); + EXPECT_EQ(Tracker::TriggerState::HAS_NOT_BEEN_DISPLAYED, + tracker_->GetTriggerState(kTestFeatureBar)); + + // Dismiss foo and show qux, which should update TriggerState of bar, and keep + // TriggerState for foo. + tracker_->Dismissed(kTestFeatureFoo); + EXPECT_TRUE(tracker_->ShouldTriggerHelpUI(kTestFeatureBar)); + VerifyEventTriggerEvents(kTestFeatureBar, 1); + EXPECT_EQ(Tracker::TriggerState::HAS_BEEN_DISPLAYED, + tracker_->GetTriggerState(kTestFeatureFoo)); + EXPECT_EQ(Tracker::TriggerState::HAS_BEEN_DISPLAYED, + tracker_->GetTriggerState(kTestFeatureBar)); +} + +TEST_F(TrackerImplTest, TestNotifyEvent) { + StoringInitializedCallback callback; + tracker_->AddOnInitializedCallback(base::Bind( + &StoringInitializedCallback::OnInitialized, base::Unretained(&callback))); + base::RunLoop().RunUntilIdle(); + base::UserActionTester user_action_tester; + + tracker_->NotifyEvent("foo"); + tracker_->NotifyEvent("foo"); + tracker_->NotifyEvent("bar"); + tracker_->NotifyEvent(kTestFeatureFoo.name + std::string("_used")); + tracker_->NotifyEvent(kTestFeatureFoo.name + std::string("_trigger")); + + // Used event will record both NotifyEvent and NotifyUsedEvent. Explicitly + // specify the whole user action string here. + EXPECT_EQ(1, user_action_tester.GetActionCount( + "InProductHelp.NotifyUsedEvent.test_foo")); + EXPECT_EQ(2, user_action_tester.GetActionCount( + "InProductHelp.NotifyEvent.test_foo")); + EXPECT_EQ(0, user_action_tester.GetActionCount( + "InProductHelp.NotifyUsedEvent.test_bar")); + EXPECT_EQ(0, user_action_tester.GetActionCount( + "InProductHelp.NotifyEvent.test_bar")); + + Event foo_event = event_store_->GetEvent("foo"); + ASSERT_EQ(1, foo_event.events_size()); + EXPECT_EQ(1u, foo_event.events(0).day()); + EXPECT_EQ(2u, foo_event.events(0).count()); + + Event bar_event = event_store_->GetEvent("bar"); + ASSERT_EQ(1, bar_event.events_size()); + EXPECT_EQ(1u, bar_event.events(0).day()); + EXPECT_EQ(1u, bar_event.events(0).count()); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/public/BUILD.gn b/chromium/components/feature_engagement/public/BUILD.gn new file mode 100644 index 00000000000..82fffd5bc4d --- /dev/null +++ b/chromium/components/feature_engagement/public/BUILD.gn @@ -0,0 +1,51 @@ +# Copyright 2017 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if (is_android) { + import("//build/config/android/config.gni") + import("//build/config/android/rules.gni") +} + +source_set("public") { + sources = [ + "event_constants.cc", + "event_constants.h", + "feature_constants.cc", + "feature_constants.h", + "feature_list.cc", + "feature_list.h", + "tracker.h", + ] + + deps = [ + "//base", + "//components/flags_ui", + "//components/keyed_service/core", + ] +} + +if (is_android) { + android_library("public_java") { + java_files = [ + "android/java/src/org/chromium/components/feature_engagement/EventConstants.java", + "android/java/src/org/chromium/components/feature_engagement/FeatureConstants.java", + "android/java/src/org/chromium/components/feature_engagement/Tracker.java", + ] + + deps = [ + "//base:base_java", + "//third_party/android_tools:android_support_annotations_java", + ] + + srcjar_deps = [ ":public_java_enums_srcjar" ] + } + + java_cpp_enum("public_java_enums_srcjar") { + visibility = [ ":*" ] + + sources = [ + "tracker.h", + ] + } +} diff --git a/chromium/components/feature_engagement/public/event_constants.cc b/chromium/components/feature_engagement/public/event_constants.cc new file mode 100644 index 00000000000..b2fb65c5a6f --- /dev/null +++ b/chromium/components/feature_engagement/public/event_constants.cc @@ -0,0 +1,33 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/public/event_constants.h" + +namespace feature_engagement { + +namespace events { + +#if defined(OS_WIN) || defined(OS_LINUX) +const char kOmniboxInteraction[] = "omnibox_used"; +const char kNewTabSessionTimeMet[] = "new_tab_session_time_met"; + +const char kHistoryDeleted[] = "history_deleted"; +const char kIncognitoWindowOpened[] = "incognito_window_opened"; + +#endif // defined(OS_WIN) || defined(OS_LINUX) + +#if defined(OS_WIN) || defined(OS_LINUX) || defined(OS_IOS) +const char kNewTabOpened[] = "new_tab_opened"; +#endif // defined(OS_WIN) || defined(OS_LINUX) || defined(OS_IOS) + +#if defined(OS_IOS) +const char kChromeOpened[] = "chrome_opened"; +const char kIncognitoTabOpened[] = "incognito_tab_opened"; +const char kClearedBrowsingData[] = "cleared_browsing_data"; +const char kViewedReadingList[] = "viewed_reading_list"; +#endif // defined(OS_IOS) + +} // namespace events + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/public/event_constants.h b/chromium/components/feature_engagement/public/event_constants.h new file mode 100644 index 00000000000..e6d514b2f41 --- /dev/null +++ b/chromium/components/feature_engagement/public/event_constants.h @@ -0,0 +1,65 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_PUBLIC_EVENT_CONSTANTS_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_PUBLIC_EVENT_CONSTANTS_H_ + +#include "build/build_config.h" + +namespace feature_engagement { + +namespace events { + +#if defined(OS_WIN) || defined(OS_LINUX) +// All the events declared below are the string names +// of deferred onboarding events for the New Tab. + +// The user has interacted with the omnibox. +extern const char kOmniboxInteraction[]; +// The user has satisfied the session time requirement to show the NewTabPromo +// by accumulating 2 hours of active session time (one-off event). +extern const char kNewTabSessionTimeMet[]; + +// All the events declared below are the string names +// of deferred onboarding events for the Incognito Window + +// The user has deleted browsing history. +extern const char kHistoryDeleted[]; +// The user has opened an incognito window. +extern const char kIncognitoWindowOpened[]; + +#endif // defined(OS_WIN) || defined(OS_LINUX) + +#if defined(OS_WIN) || defined(OS_LINUX) || defined(OS_IOS) +// This event is included in the deferred onboarding events for the New Tab +// described above, but it is also used on iOS, so it must be compiled +// separately. + +// The user has explicitly opened a new tab via an entry point from inside of +// Chrome. +extern const char kNewTabOpened[]; + +#endif // defined(OS_WIN) || defined(OS_LINUX) || defined(OS_IOS) + +#if defined(OS_IOS) + +// The user has opened Chrome (cold start or from background). +extern const char kChromeOpened[]; + +// The user has opened an incognito tab. +extern const char kIncognitoTabOpened[]; + +// The user has cleared their browsing data. +extern const char kClearedBrowsingData[]; + +// The user has viewed their reading list. +extern const char kViewedReadingList[]; + +#endif // defined(OS_IOS) + +} // namespace events + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_PUBLIC_EVENT_CONSTANTS_H_ diff --git a/chromium/components/feature_engagement/public/feature_constants.cc b/chromium/components/feature_engagement/public/feature_constants.cc new file mode 100644 index 00000000000..eb66d1f38a3 --- /dev/null +++ b/chromium/components/feature_engagement/public/feature_constants.cc @@ -0,0 +1,48 @@ +/// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/public/feature_constants.h" + +namespace feature_engagement { + +const base::Feature kIPHDemoMode{"IPH_DemoMode", + base::FEATURE_DISABLED_BY_DEFAULT}; + +const base::Feature kIPHDummyFeature{"IPH_Dummy", + base::FEATURE_DISABLED_BY_DEFAULT}; + +#if defined(OS_ANDROID) +const base::Feature kIPHDataSaverDetailFeature{ + "IPH_DataSaverDetail", base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kIPHDataSaverPreviewFeature{ + "IPH_DataSaverPreview", base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kIPHDownloadHomeFeature{"IPH_DownloadHome", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kIPHDownloadPageFeature{"IPH_DownloadPage", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kIPHDownloadPageScreenshotFeature{ + "IPH_DownloadPageScreenshot", base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kIPHChromeHomeExpandFeature{ + "IPH_ChromeHomeExpand", base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kIPHMediaDownloadFeature{"IPH_MediaDownload", + base::FEATURE_DISABLED_BY_DEFAULT}; +#endif // defined(OS_ANDROID) + +#if defined(OS_WIN) || defined(OS_LINUX) +const base::Feature kIPHIncognitoWindowFeature{ + "IPH_IncognitoWindow", base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kIPHNewTabFeature{"IPH_NewTab", + base::FEATURE_DISABLED_BY_DEFAULT}; +#endif // defined(OS_WIN) || defined(OS_LINUX) + +#if defined(OS_IOS) +const base::Feature kIPHNewTabTipFeature{"IPH_NewTabTip", + base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kIPHNewIncognitoTabTipFeature{ + "IPH_NewIncognitoTabTip", base::FEATURE_DISABLED_BY_DEFAULT}; +const base::Feature kIPHBadgedReadingListFeature{ + "IPH_BadgedReadingList", base::FEATURE_DISABLED_BY_DEFAULT}; +#endif // defined(OS_IOS) + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/public/feature_constants.h b/chromium/components/feature_engagement/public/feature_constants.h new file mode 100644 index 00000000000..52052f50cd5 --- /dev/null +++ b/chromium/components/feature_engagement/public/feature_constants.h @@ -0,0 +1,45 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_PUBLIC_FEATURE_CONSTANTS_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_PUBLIC_FEATURE_CONSTANTS_H_ + +#include "base/feature_list.h" +#include "build/build_config.h" + +namespace feature_engagement { + +// A feature for enabling a demonstration mode for In-Product Help (IPH). +extern const base::Feature kIPHDemoMode; + +// A feature to ensure all arrays can contain at least one feature. +extern const base::Feature kIPHDummyFeature; + +// All the features declared for Android below that are also used in Java, +// should also be declared in: +// org.chromium.components.feature_engagement.FeatureConstants. +#if defined(OS_ANDROID) +extern const base::Feature kIPHDataSaverDetailFeature; +extern const base::Feature kIPHDataSaverPreviewFeature; +extern const base::Feature kIPHDownloadHomeFeature; +extern const base::Feature kIPHDownloadPageFeature; +extern const base::Feature kIPHDownloadPageScreenshotFeature; +extern const base::Feature kIPHChromeHomeExpandFeature; +extern const base::Feature kIPHMediaDownloadFeature; +#endif // defined(OS_ANDROID) + +#if defined(OS_WIN) || defined(OS_LINUX) +extern const base::Feature kIPHIncognitoWindowFeature; +extern const base::Feature kIPHNewTabFeature; +#endif // defined(OS_WIN) || defined(OS_LINUX) + +#if defined(OS_IOS) +extern const base::Feature kIPHNewTabTipFeature; +extern const base::Feature kIPHNewIncognitoTabTipFeature; +extern const base::Feature kIPHBadgedReadingListFeature; +#endif // defined(OS_IOS) + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_PUBLIC_FEATURE_CONSTANTS_H_ diff --git a/chromium/components/feature_engagement/public/feature_list.cc b/chromium/components/feature_engagement/public/feature_list.cc new file mode 100644 index 00000000000..d2971fe97ec --- /dev/null +++ b/chromium/components/feature_engagement/public/feature_list.cc @@ -0,0 +1,45 @@ +/// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/feature_engagement/public/feature_list.h" + +#include "components/feature_engagement/public/feature_constants.h" + +namespace feature_engagement { + +namespace { +// Whenever a feature is added to |kAllFeatures|, it should also be added as +// DEFINE_VARIATION_PARAM in the header, and also added to the +// |kIPHDemoModeChoiceVariations| array. +const base::Feature* const kAllFeatures[] = { + &kIPHDummyFeature, // Ensures non-empty array for all platforms. +#if defined(OS_ANDROID) + &kIPHDataSaverDetailFeature, + &kIPHDataSaverPreviewFeature, + &kIPHDownloadHomeFeature, + &kIPHDownloadPageFeature, + &kIPHDownloadPageScreenshotFeature, + &kIPHChromeHomeExpandFeature, + &kIPHMediaDownloadFeature, +#endif // defined(OS_ANDROID) +#if defined(OS_WIN) || defined(OS_LINUX) + &kIPHIncognitoWindowFeature, + &kIPHNewTabFeature, +#endif // defined(OS_WIN) || defined(OS_LINUX) +#if defined(OS_IOS) + &kIPHNewTabTipFeature, + &kIPHNewIncognitoTabTipFeature, + &kIPHBadgedReadingListFeature, +#endif // defined(OS_IOS) +}; +} // namespace + +const char kIPHDemoModeFeatureChoiceParam[] = "chosen_feature"; + +std::vector<const base::Feature*> GetAllFeatures() { + return std::vector<const base::Feature*>( + kAllFeatures, kAllFeatures + arraysize(kAllFeatures)); +} + +} // namespace feature_engagement diff --git a/chromium/components/feature_engagement/public/feature_list.h b/chromium/components/feature_engagement/public/feature_list.h new file mode 100644 index 00000000000..b260327ff22 --- /dev/null +++ b/chromium/components/feature_engagement/public/feature_list.h @@ -0,0 +1,102 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_PUBLIC_FEATURE_LIST_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_PUBLIC_FEATURE_LIST_H_ + +#include <vector> + +#include "base/feature_list.h" +#include "build/build_config.h" +#include "components/feature_engagement/public/feature_constants.h" +#include "components/flags_ui/feature_entry.h" + +namespace feature_engagement { +using FeatureVector = std::vector<const base::Feature*>; + +// The param name for the FeatureVariation configuration, which is used by +// chrome://flags to set the variable name for the selected feature. The Tracker +// backend will then read this to figure out which feature (if any) was selected +// by the end user. +extern const char kIPHDemoModeFeatureChoiceParam[]; + +namespace { + +// Defines a const flags_ui::FeatureEntry::FeatureParam for the given +// base::Feature. The constant name will be on the form +// kFooFeature --> kFooFeatureVariation. The |feature_name| argument must +// match the base::Feature::name member of the |base_feature|. +// This is intended to be used with VARIATION_ENTRY below to be able to insert +// it into an array of flags_ui::FeatureEntry::FeatureVariation. +#define DEFINE_VARIATION_PARAM(base_feature, feature_name) \ + constexpr flags_ui::FeatureEntry::FeatureParam base_feature##Variation[] = { \ + {kIPHDemoModeFeatureChoiceParam, feature_name}} + +// Defines a single flags_ui::FeatureEntry::FeatureVariation entry, fully +// enclosed. This is intended to be used with the declaration of +// |kIPHDemoModeChoiceVariations| below. +#define VARIATION_ENTRY(base_feature) \ + { \ + base_feature##Variation[0].param_value, base_feature##Variation, \ + arraysize(base_feature##Variation), nullptr \ + } + +// Defines a flags_ui::FeatureEntry::FeatureParam for each feature. +DEFINE_VARIATION_PARAM(kIPHDummyFeature, "IPH_Dummy"); +#if defined(OS_ANDROID) +DEFINE_VARIATION_PARAM(kIPHDataSaverDetailFeature, "IPH_DataSaverDetail"); +DEFINE_VARIATION_PARAM(kIPHDataSaverPreviewFeature, "IPH_DataSaverPreview"); +DEFINE_VARIATION_PARAM(kIPHDownloadHomeFeature, "IPH_DownloadHome"); +DEFINE_VARIATION_PARAM(kIPHDownloadPageFeature, "IPH_DownloadPage"); +DEFINE_VARIATION_PARAM(kIPHDownloadPageScreenshotFeature, + "IPH_DownloadPageScreenshot"); +DEFINE_VARIATION_PARAM(kIPHChromeHomeExpandFeature, "IPH_ChromeHomeExpand"); +DEFINE_VARIATION_PARAM(kIPHMediaDownloadFeature, "IPH_MediaDownload"); +#endif // defined(OS_ANDROID) +#if defined(OS_WIN) || defined(OS_LINUX) +DEFINE_VARIATION_PARAM(kIPHIncognitoWindowFeature, "IPH_IncognitoWindow"); +DEFINE_VARIATION_PARAM(kIPHNewTabFeature, "IPH_NewTab"); +#endif // defined(OS_WIN) || defined(OS_LINUX) +#if defined(OS_IOS) +DEFINE_VARIATION_PARAM(kIPHNewTabTipFeature, "IPH_NewTabTip"); +DEFINE_VARIATION_PARAM(kIPHNewIncognitoTabTipFeature, "IPH_NewIncognitoTabTip"); +DEFINE_VARIATION_PARAM(kIPHBadgedReadingListFeature, "IPH_BadgedReadingList"); +#endif // defined(OS_IOS) + +} // namespace + +// Defines the array of which features should be listed in the chrome://flags +// UI to be able to select them alone for demo-mode. The features listed here +// are possible to enable on their own in demo mode. +constexpr flags_ui::FeatureEntry::FeatureVariation + kIPHDemoModeChoiceVariations[] = { +#if defined(OS_ANDROID) + VARIATION_ENTRY(kIPHDataSaverDetailFeature), + VARIATION_ENTRY(kIPHDataSaverPreviewFeature), + VARIATION_ENTRY(kIPHDownloadHomeFeature), + VARIATION_ENTRY(kIPHDownloadPageFeature), + VARIATION_ENTRY(kIPHDownloadPageScreenshotFeature), + VARIATION_ENTRY(kIPHChromeHomeExpandFeature), + VARIATION_ENTRY(kIPHMediaDownloadFeature), +#elif defined(OS_WIN) || defined(OS_LINUX) + VARIATION_ENTRY(kIPHIncognitoWindowFeature), + VARIATION_ENTRY(kIPHNewTabFeature), +#elif defined(OS_IOS) + VARIATION_ENTRY(kIPHNewTabTipFeature), + VARIATION_ENTRY(kIPHNewIncognitoTabTipFeature), + VARIATION_ENTRY(kIPHBadgedReadingListFeature), +#else + VARIATION_ENTRY(kIPHDummyFeature), // Ensures non-empty array. +#endif +}; + +#undef DEFINE_VARIATION_PARAM +#undef VARIATION_ENTRY + +// Returns all the features that are in use for engagement tracking. +FeatureVector GetAllFeatures(); + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_PUBLIC_FEATURE_LIST_H_ diff --git a/chromium/components/feature_engagement/public/tracker.h b/chromium/components/feature_engagement/public/tracker.h new file mode 100644 index 00000000000..22040a51ac8 --- /dev/null +++ b/chromium/components/feature_engagement/public/tracker.h @@ -0,0 +1,111 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef COMPONENTS_FEATURE_ENGAGEMENT_PUBLIC_TRACKER_H_ +#define COMPONENTS_FEATURE_ENGAGEMENT_PUBLIC_TRACKER_H_ + +#include <string> + +#include "base/callback.h" +#include "base/compiler_specific.h" +#include "base/feature_list.h" +#include "base/files/file_path.h" +#include "base/memory/ref_counted.h" +#include "base/sequenced_task_runner.h" +#include "build/build_config.h" +#include "components/keyed_service/core/keyed_service.h" + +#if defined(OS_ANDROID) +#include "base/android/jni_android.h" +#endif // defined(OS_ANDROID) + +namespace feature_engagement { + +// The Tracker provides a backend for displaying feature +// enlightenment or in-product help (IPH) with a clean and easy to use API to be +// consumed by the UI frontend. The backend behaves as a black box and takes +// input about user behavior. Whenever the frontend gives a trigger signal that +// IPH could be displayed, the backend will provide an answer to whether it is +// appropriate to show it or not. +class Tracker : public KeyedService { + public: + // Describes the state of whether in-product helps has already been displayed + // enough times or not within the bounds of the configuration for a + // base::Feature. NOT_READY is returned if the Tracker has not been + // initialized yet before the call to GetTriggerState(...). + // GENERATED_JAVA_ENUM_PACKAGE: org.chromium.components.feature_engagement + enum class TriggerState : int { + HAS_BEEN_DISPLAYED = 0, + HAS_NOT_BEEN_DISPLAYED = 1, + NOT_READY = 2 + }; + +#if defined(OS_ANDROID) + // Returns a Java object of the type Tracker for the given Tracker. + static base::android::ScopedJavaLocalRef<jobject> GetJavaObject( + Tracker* feature_engagement); +#endif // defined(OS_ANDROID) + + // Invoked when the tracker has been initialized. The |success| parameter + // indicates that the initialization was a success and the tracker is ready to + // receive calls. + using OnInitializedCallback = base::Callback<void(bool success)>; + + // The |storage_dir| is the path to where all local storage will be. + // The |bakground_task_runner| will be used for all disk reads and writes. + static Tracker* Create( + const base::FilePath& storage_dir, + const scoped_refptr<base::SequencedTaskRunner>& background_task_runner); + + // Must be called whenever an event happens. + virtual void NotifyEvent(const std::string& event) = 0; + + // This function must be called whenever the triggering condition for a + // specific feature happens. Returns true iff the display of the in-product + // help must happen. + // If |true| is returned, the caller *must* call Dismissed() when display + // of feature enlightenment ends. + virtual bool ShouldTriggerHelpUI(const base::Feature& feature) + WARN_UNUSED_RESULT = 0; + + // This function can be called to query if a particular |feature| meets its + // particular precondition for triggering within the bounds of the current + // feature configuration. + // Calling this method requires the Tracker to already have been initialized. + // See IsInitialized() and AddOnInitializedCallback(...) for how to ensure + // the call to this is delayed. + // This function can typically be used to ensure that expensive operations + // for tracking other state related to in-product help do not happen if + // in-product help has already been displayed for the given |feature|. + virtual TriggerState GetTriggerState(const base::Feature& feature) = 0; + + // Must be called after display of feature enlightenment finishes for a + // particular |feature|. + virtual void Dismissed(const base::Feature& feature) = 0; + + // Returns whether the tracker has been successfully initialized. During + // startup, this will be false until the internal models have been loaded at + // which point it is set to true if the initialization was successful. The + // state will never change from initialized to uninitialized. + // Callers can invoke AddOnInitializedCallback(...) to be notified when the + // result of the initialization is ready. + virtual bool IsInitialized() = 0; + + // For features that trigger on startup, they can register a callback to + // ensure that they are informed when the tracker has finished the + // initialization. If the tracker has already been initialized, the callback + // will still be invoked with the result. The callback is guaranteed to be + // invoked exactly one time. + virtual void AddOnInitializedCallback(OnInitializedCallback callback) = 0; + + protected: + Tracker() = default; + + private: + DISALLOW_COPY_AND_ASSIGN(Tracker); +}; + +} // namespace feature_engagement + +#endif // COMPONENTS_FEATURE_ENGAGEMENT_PUBLIC_TRACKER_H_ diff --git a/chromium/components/feature_engagement/test/BUILD.gn b/chromium/components/feature_engagement/test/BUILD.gn new file mode 100644 index 00000000000..1c9986b7413 --- /dev/null +++ b/chromium/components/feature_engagement/test/BUILD.gn @@ -0,0 +1,18 @@ +# Copyright 2017 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +source_set("test_support") { + testonly = true + + sources = [ + "test_tracker.cc", + "test_tracker.h", + ] + + deps = [ + "//base", + "//components/feature_engagement/internal", + "//components/feature_engagement/public", + ] +} |