summaryrefslogtreecommitdiff
path: root/chromium/components/feature_engagement
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2017-11-20 10:33:36 +0100
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2017-11-22 11:45:12 +0000
commitbe59a35641616a4cf23c4a13fa0632624b021c1b (patch)
tree9da183258bdf9cc413f7562079d25ace6955467f /chromium/components/feature_engagement
parentd702e4b6a64574e97fc7df8fe3238cde70242080 (diff)
downloadqtwebengine-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')
-rw-r--r--chromium/components/feature_engagement/BUILD.gn47
-rw-r--r--chromium/components/feature_engagement/DEPS7
-rw-r--r--chromium/components/feature_engagement/OWNERS5
-rw-r--r--chromium/components/feature_engagement/README.md489
-rw-r--r--chromium/components/feature_engagement/components_unittests.filter22
-rw-r--r--chromium/components/feature_engagement/internal/BUILD.gn145
-rw-r--r--chromium/components/feature_engagement/internal/android/tracker_impl_android.cc141
-rw-r--r--chromium/components/feature_engagement/internal/android/tracker_impl_android.h80
-rw-r--r--chromium/components/feature_engagement/internal/availability_model.h54
-rw-r--r--chromium/components/feature_engagement/internal/availability_model_impl.cc62
-rw-r--r--chromium/components/feature_engagement/internal/availability_model_impl.h69
-rw-r--r--chromium/components/feature_engagement/internal/availability_model_impl_unittest.cc134
-rw-r--r--chromium/components/feature_engagement/internal/chrome_variations_configuration.cc334
-rw-r--r--chromium/components/feature_engagement/internal/chrome_variations_configuration.h48
-rw-r--r--chromium/components/feature_engagement/internal/chrome_variations_configuration_unittest.cc631
-rw-r--r--chromium/components/feature_engagement/internal/condition_validator.cc57
-rw-r--r--chromium/components/feature_engagement/internal/condition_validator.h99
-rw-r--r--chromium/components/feature_engagement/internal/condition_validator_unittest.cc86
-rw-r--r--chromium/components/feature_engagement/internal/configuration.cc135
-rw-r--r--chromium/components/feature_engagement/internal/configuration.h149
-rw-r--r--chromium/components/feature_engagement/internal/configuration_unittest.cc81
-rw-r--r--chromium/components/feature_engagement/internal/editable_configuration.cc44
-rw-r--r--chromium/components/feature_engagement/internal/editable_configuration.h46
-rw-r--r--chromium/components/feature_engagement/internal/editable_configuration_unittest.cc77
-rw-r--r--chromium/components/feature_engagement/internal/event_model.h56
-rw-r--r--chromium/components/feature_engagement/internal/event_model_impl.cc135
-rw-r--r--chromium/components/feature_engagement/internal/event_model_impl.h68
-rw-r--r--chromium/components/feature_engagement/internal/event_model_impl_unittest.cc467
-rw-r--r--chromium/components/feature_engagement/internal/event_storage_validator.h40
-rw-r--r--chromium/components/feature_engagement/internal/event_store.h48
-rw-r--r--chromium/components/feature_engagement/internal/feature_config_condition_validator.cc122
-rw-r--r--chromium/components/feature_engagement/internal/feature_config_condition_validator.h56
-rw-r--r--chromium/components/feature_engagement/internal/feature_config_condition_validator_unittest.cc538
-rw-r--r--chromium/components/feature_engagement/internal/feature_config_event_storage_validator.cc90
-rw-r--r--chromium/components/feature_engagement/internal/feature_config_event_storage_validator.h63
-rw-r--r--chromium/components/feature_engagement/internal/feature_config_event_storage_validator_unittest.cc295
-rw-r--r--chromium/components/feature_engagement/internal/in_memory_event_store.cc51
-rw-r--r--chromium/components/feature_engagement/internal/in_memory_event_store.h48
-rw-r--r--chromium/components/feature_engagement/internal/in_memory_event_store_unittest.cc70
-rw-r--r--chromium/components/feature_engagement/internal/init_aware_event_model.cc69
-rw-r--r--chromium/components/feature_engagement/internal/init_aware_event_model.h54
-rw-r--r--chromium/components/feature_engagement/internal/init_aware_event_model_unittest.cc170
-rw-r--r--chromium/components/feature_engagement/internal/never_availability_model.cc45
-rw-r--r--chromium/components/feature_engagement/internal/never_availability_model.h43
-rw-r--r--chromium/components/feature_engagement/internal/never_availability_model_unittest.cc73
-rw-r--r--chromium/components/feature_engagement/internal/never_condition_validator.cc26
-rw-r--r--chromium/components/feature_engagement/internal/never_condition_validator.h43
-rw-r--r--chromium/components/feature_engagement/internal/never_condition_validator_unittest.cc71
-rw-r--r--chromium/components/feature_engagement/internal/never_event_storage_validator.cc24
-rw-r--r--chromium/components/feature_engagement/internal/never_event_storage_validator.h34
-rw-r--r--chromium/components/feature_engagement/internal/never_event_storage_validator_unittest.cc36
-rw-r--r--chromium/components/feature_engagement/internal/once_condition_validator.cc49
-rw-r--r--chromium/components/feature_engagement/internal/once_condition_validator.h63
-rw-r--r--chromium/components/feature_engagement/internal/once_condition_validator_unittest.cc155
-rw-r--r--chromium/components/feature_engagement/internal/persistent_availability_store.cc158
-rw-r--r--chromium/components/feature_engagement/internal/persistent_availability_store.h61
-rw-r--r--chromium/components/feature_engagement/internal/persistent_availability_store_unittest.cc295
-rw-r--r--chromium/components/feature_engagement/internal/persistent_event_store.cc91
-rw-r--r--chromium/components/feature_engagement/internal/persistent_event_store.h59
-rw-r--r--chromium/components/feature_engagement/internal/persistent_event_store_unittest.cc251
-rw-r--r--chromium/components/feature_engagement/internal/proto/BUILD.gn12
-rw-r--r--chromium/components/feature_engagement/internal/proto/availability.proto21
-rw-r--r--chromium/components/feature_engagement/internal/proto/event.proto27
-rw-r--r--chromium/components/feature_engagement/internal/single_invalid_configuration.cc33
-rw-r--r--chromium/components/feature_engagement/internal/single_invalid_configuration.h46
-rw-r--r--chromium/components/feature_engagement/internal/single_invalid_configuration_unittest.cc41
-rw-r--r--chromium/components/feature_engagement/internal/stats.cc210
-rw-r--r--chromium/components/feature_engagement/internal/stats.h150
-rw-r--r--chromium/components/feature_engagement/internal/system_time_provider.cc24
-rw-r--r--chromium/components/feature_engagement/internal/system_time_provider.h34
-rw-r--r--chromium/components/feature_engagement/internal/system_time_provider_unittest.cc113
-rw-r--r--chromium/components/feature_engagement/internal/test/BUILD.gn19
-rw-r--r--chromium/components/feature_engagement/internal/time_provider.h31
-rw-r--r--chromium/components/feature_engagement/internal/tracker_impl.cc270
-rw-r--r--chromium/components/feature_engagement/internal/tracker_impl.h92
-rw-r--r--chromium/components/feature_engagement/internal/tracker_impl_unittest.cc649
-rw-r--r--chromium/components/feature_engagement/public/BUILD.gn51
-rw-r--r--chromium/components/feature_engagement/public/event_constants.cc33
-rw-r--r--chromium/components/feature_engagement/public/event_constants.h65
-rw-r--r--chromium/components/feature_engagement/public/feature_constants.cc48
-rw-r--r--chromium/components/feature_engagement/public/feature_constants.h45
-rw-r--r--chromium/components/feature_engagement/public/feature_list.cc45
-rw-r--r--chromium/components/feature_engagement/public/feature_list.h102
-rw-r--r--chromium/components/feature_engagement/public/tracker.h111
-rw-r--r--chromium/components/feature_engagement/test/BUILD.gn18
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, &params);
+ 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",
+ ]
+}