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.
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:
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.
You need to do the following things to enable your feature, all described in detail below.
feature_engagement::Tracker
.feature_engagement::Tracker
class by notifying about events, and checking whether In-Product Help should be displayed.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:
kIPH
prefixMyFun
.Feature
suffix.The name member of the base::Feature
struct should match the constant name, and be on the form:
IPH_
prefixMyFun
.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
:
const base::Feature kIPHMyFunFeature{"IPH_MyFun", base::FEATURE_DISABLED_BY_DEFAULT};
//components/feature_engagement/public/feature_constants.h
:
extern const base::Feature kIPHMyFunFeature;
//components/feature_engagement/public/feature_list.cc
:
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.
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
:
feature_engagement::Tracker* tracker = feature_engagement::TrackerFactory::GetForBrowserContext(profile);
That service can be first of all used to notify the backend about events:
tracker->NotifyEvent("your_event_name");
In addition, it can tell you whether it is a good time to trigger the help UI:
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:
tracker->Dismissed(feature_engagement::kIPHMyFunFeature);
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:
// 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.
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.Another way to check the internal state of the Tracker
is to invoke feature_engagement::Tracker::WouldTriggerHelpUI
which is basically the same as invoking feature_engagement::Tracker::ShouldTriggerHelpUI
, but being allowed to ignore the state. It is still required to invoke feature_engagement::Tracker::ShouldTriggerHelpUI
if in-product help should be shown.
WARNING: It is not guaranteed that invoking
ShouldTriggerHelpUI(...)
after this would yield the same result. The state might change in-between the calls because time has passed, other events might have been triggered, and other state might have changed.
To enable UMA tracking, you need to make the following changes to the metrics configuration:
IPHFeatures
in: //tools/metrics/histograms/histograms.xml
.base::Feature
name
member of your feature.//tools/metrics/actions/actions.xml
.base::Feature
name
member.<action-suffix>
entry at the end of the file, where the following <affected-action>
s are listed:InProductHelp.NotifyEvent.IPH
InProductHelp.NotifyUsedEvent.IPH
InProductHelp.ShouldTriggerHelpUI.IPH
InProductHelp.ShouldTriggerHelpUIResult.NotTriggered.IPH
InProductHelp.ShouldTriggerHelpUIResult.Triggered.IPH
InProductHelp.ShouldTriggerHelpUIResult.WouldHaveTriggered.IPH
<suffix>
es like: <suffix name="MyFunFeature" label="For MyFunFeature feature."/>
For each in-product help feature, it is required to also configure the expected launch configuration as the main testing configuration. See Field Trial Testing Configuration for details.
Basically this requires you to add a new section to //testing/variations/fieldtrial_testing_config.json
for your feature. The format is described in the documentation linked above, but it will probably look something like this:
{ "MyFunFeatureStudy": [ { "platforms": ["android"], "experiments": [ { "name": "MyFunFeatureLaunchConfig", "params": { "availability": ">=30", "session_rate": "<1", "event_used": "name:fun_event_happened;comparator:any;window:360;storage:360", "event_trigger": "name:fun_feature_iph_triggered;comparator:any;window:360;storage:360", "event_1": "name:related_fun_thing_happened;comparator:>=1;window:360;storage:360" }, "enable_features": ["IPH_MyFunFeature"], "disable_features": [] } ] } ], ... }
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:
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.
IPH_MyFunFeature
you would select:Each In-Product Help feature must have its own feature configuration 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.
Format:
{ "availability": "{Comparator}", "session_rate": "{Comparator}", "session_rate_impact": "{SessionRateImpact}", "event_used": "{EventConfig}", "event_trigger": "{EventConfig}", "event_???": "{EventConfig}", "tracking_only": "{Boolean}" "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
REQUIREDComparator
is in a number of days.session_rate
REQUIREDComparator
is a count of total In-Product Help displayed in the current end user session.session_rate_impact
all
.event_used
REQUIREDevent_trigger
REQUIREDevent_???
event_
items, but for all other preconditions that must have been met./^event_[a-zA-Z0-9-_]+$/
and not be event_used
or event_trigger
.tracking_only
true
to in fact display in-product help, you might want to use a different EventConfig::name
for the event_trigger
configuration than the non-tracking configuration.false
.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" }
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
/^[a-zA-Z0-9-_]+$/
comparator
window
0
Nothing should be counted.1
|current_day| should be counted.2+
|current_day| plus |N-1| more days should be counted.storage
0
Nothing should be stored.1
|current_day| should be stored.2+
|current_day| plus |N-1| more days should be stored.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
Format: {COMPARATOR}[value]
The following comparators are allowed:
<
less than>
greater than<=
less than or equal>=
greater than or equal==
equal!=
not equalany
always true (no value allowed)Other than any
, all comparators require a value.
Examples
>=10 ==0 any <15
Format: [true|false]
The following values are allowed:
true
false
The value must be quoted (like all the other values).
Examples
true false TRUE FALSE True False
Format: [all|none|comma-separated list]
all
means this feature impacts every other feature regarding their session_rate
calculations. This is the default.none
means that this feature does not impact any other features regarding the session_rate
. This feature may therefore be shown an unlimited amount of times, without making other features go over their session_rate
config.[comma-separated list]
means that this feature only impacts the particular features listed. Use the base::Feature
name of the feature in the list. For features in the list, this feature will affect their session_rate
conditions, and for features not in the list, this feature will not affect their session_rate
calculations.all
or none
. They must only be used alone with no comma, at which point they work as described above.Examples
all none IPH_DownloadHome IPH_DonwloadPage,IPH_DownloadHome
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.
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
:
{ "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": [] } ] } ] }
Use the field trial utility to convert the JSON configuration to command line arguments:
python ./tools/variations/fieldtrial_util.py DownloadStudy.json android shell_cmd
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:
./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"
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:
--vmodule=tracker_impl*=2,event_model_impl*=2,persistent_availability_store*=2,chrome_variations_configuration*=3
//components/feature_engagement
To compile and run tests, assuming the product out directory is out/Debug
, use:
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
.