| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifndef CHROME_BROWSER_WEB_APPLICATIONS_WEB_APP_PREF_GUARDRAILS_H_ |
| #define CHROME_BROWSER_WEB_APPLICATIONS_WEB_APP_PREF_GUARDRAILS_H_ |
| |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/raw_ref.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/web_applications/web_app_constants.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/prefs/scoped_user_pref_update.h" |
| #include "components/webapps/common/web_app_id.h" |
| |
| class PrefService; |
| |
| namespace user_prefs { |
| class PrefRegistrySyncable; |
| } |
| |
| namespace web_app { |
| |
| struct GuardrailData { |
| std::optional<int> app_specific_not_accept_count; |
| std::optional<int> app_specific_mute_after_dismiss_days; |
| std::optional<int> app_specific_mute_after_ignore_days; |
| int global_not_accept_count; |
| std::optional<int> global_mute_after_dismiss_days; |
| std::optional<int> global_mute_after_ignore_days; |
| }; |
| |
| struct GuardrailPrefNames { |
| std::string_view last_ignore_time_name; |
| std::string_view last_dismiss_time_name; |
| std::string_view not_accepted_count_name; |
| std::string_view all_blocked_time_name; |
| std::string_view global_pref_name; |
| std::string_view block_reason_name; |
| }; |
| |
| std::optional<int> GetIntWebAppPref(const PrefService* pref_service, |
| const webapps::AppId& app_id, |
| std::string_view path); |
| |
| std::optional<base::Time> GetTimeWebAppPref(const PrefService* pref_service, |
| const webapps::AppId& app_id, |
| std::string_view path); |
| |
| // WebAppPrefGuardrails provide a simple way of building guardrails based on the |
| // number of times a prompt on an app has been ignored or dismissed in the past. |
| // The guardrails help prevent the prompt from showing up after a specific |
| // number of times based on the user behavior. Data for computing these |
| // guardrails are stored in the prefs. |
| class WebAppPrefGuardrails { |
| public: |
| // Returns an instance of the WebAppPrefGuardrails built to handle when the |
| // IPH bubble for the desktop install prompt should be shown. |
| static WebAppPrefGuardrails GetForDesktopInstallIph( |
| PrefService* pref_service); |
| |
| // Returns an instance of the WebAppPrefGuardrails built to handle when the |
| // ML triggered install prompt should be shown for web apps. |
| static WebAppPrefGuardrails GetForMlInstallPrompt(PrefService* pref_service); |
| |
| // Returns an instance of the WebAppPrefGuardrails built to handle when the |
| // IPH bubble for apps launched via link capturing should be shown. |
| static WebAppPrefGuardrails GetForNavigationCapturingIph( |
| PrefService* pref_service); |
| |
| // The time values are stored as a string-flavored base::value representing |
| // the int64_t number of microseconds since the Windows epoch, using |
| // base::TimeToValue(). The stored preferences look like: |
| // "web_app_ids": { |
| // "<app_id_1>": { |
| // "was_external_app_uninstalled_by_user": true, |
| // "IPH_num_of_consecutive_ignore": 2, |
| // "IPH_link_capturing_consecutive_not_accepted_num": 2, |
| // "ML_num_of_consecutive_not_accepted": 2, |
| // "IPH_last_ignore_time": "13249617864945580", |
| // "ML_last_time_install_ignored": "13249617864945580", |
| // "ML_last_time_install_dismissed": "13249617864945580", |
| // "IPH_link_capturing_last_time_ignored": "13249617864945580", |
| // "error_loaded_policy_app_migrated": true |
| // }, |
| // }, |
| // "app_agnostic_ml_state": { |
| // "ML_last_time_install_ignored": "13249617864945580", |
| // "ML_last_time_install_dismissed": "13249617864945580", |
| // "ML_num_of_consecutive_not_accepted": 2, |
| // "ML_all_promos_blocked_date": "13249617864945580", |
| // }, |
| // "app_agnostic_iph_state": { |
| // "IPH_num_of_consecutive_ignore": 3, |
| // "IPH_last_ignore_time": "13249617864945500", |
| // }, |
| // "app_agnostic_iph_link_capturing_state": { |
| // "IPH_link_capturing_consecutive_not_accepted_num": 3, |
| // "IPH_link_capturing_last_time_ignored": "13249617864945500", |
| // "IPH_link_capturing_blocked_date": "13249617864945500", |
| // "IPH_link_capturing_block_reason": |
| // "app_specific_ignore_count_hit:app_id" |
| // } |
| static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); |
| |
| ~WebAppPrefGuardrails(); |
| WebAppPrefGuardrails(const WebAppPrefGuardrails& other) = delete; |
| WebAppPrefGuardrails& operator=(const WebAppPrefGuardrails& other) = delete; |
| |
| // Record that the prompt on app corresponding to app_id being ignored at a |
| // specific time. |
| void RecordIgnore(const webapps::AppId& app_id, base::Time time); |
| // Record that the prompt on app corresponding to app_id being dismissed at a |
| // specific time. |
| void RecordDismiss(const webapps::AppId& app_id, base::Time time); |
| // Record that the prompt on app corresponding to app_id is accepted. |
| void RecordAccept(const webapps::AppId& app_id); |
| // Returns whether a new prompt should be shown or not for app_id based on |
| // values stored in the pref_names_. |
| bool IsBlockedByGuardrails(const webapps::AppId& app_id); |
| |
| private: |
| WebAppPrefGuardrails(PrefService* profile, |
| const GuardrailData& guardrail_data, |
| const GuardrailPrefNames& guardrail_pref_names, |
| std::optional<int> max_days_to_store_guardrails); |
| |
| // If guardrails are blocked, returns a string result of why it was blocked. |
| std::optional<std::string> IsAppBlocked(const webapps::AppId& app_id); |
| std::optional<std::string> IsGloballyBlocked(); |
| |
| void UpdateAppSpecificNotAcceptedPrefs(const webapps::AppId& app_id, |
| base::Time time, |
| std::string_view time_path); |
| void UpdateGlobalNotAcceptedPrefs(base::Time time, |
| std::string_view time_path); |
| |
| // If a prompt is already blocked by guardrails, return whether that should be |
| // reset. |
| bool ShouldResetGlobalGuardrails(); |
| void ResetGlobalGuardrails(const webapps::AppId& app_id); |
| |
| bool IsGlobalBlockActive(); |
| void LogGlobalBlockReason(ScopedDictPrefUpdate& global_update, |
| const std::string& reason); |
| |
| // Pref update functions. |
| void UpdateTimeWebAppPref(const webapps::AppId& app_id, |
| std::string_view path, |
| base::Time value); |
| |
| void UpdateIntWebAppPref(const webapps::AppId& app_id, |
| std::string_view path, |
| int value); |
| |
| raw_ptr<PrefService> pref_service_; |
| const raw_ref<const GuardrailData> guardrail_data_; |
| const raw_ref<const GuardrailPrefNames> pref_names_; |
| |
| // This cannot be a part of the GuardrailData struct since this is dynamic and |
| // is usually controlled via Finch, and is hence not a constant. If not |
| // defined or set to std::nullopt, guardrails will never be reset. |
| std::optional<int> max_days_to_store_guardrails_; |
| }; |
| |
| // ----------------------PWA Install IPH guardrails---------------------------- |
| // In Product Help (IPH) notifications are limited by guardrails to avoid |
| // becoming a nuisance to users. This is an overview of how they work: |
| // - Accepting the IPH bubble will not decrease further prompts, and resets |
| // existing guardrails. Otherwise: |
| // - IPH is limited globally to one every: |
| // - 14 days if prompt is ignored. |
| // - A specific site will show the IPH for installation after: |
| // - 90 days if prompt is ignored. |
| // - For a specific site, the IPH is shown 3 times at max, and then it gets |
| // blocked. |
| // - Globally, the IPH is shown 4 times at max. |
| inline constexpr GuardrailData kIphGuardrails{ |
| // Number of times IPH can be ignored for this app before it's muted. |
| .app_specific_not_accept_count = 3, |
| // Number of days to mute IPH after it's ignored for this app. |
| .app_specific_mute_after_ignore_days = 90, |
| // Number of times IPH can be ignored for any app before it's muted. |
| .global_not_accept_count = 4, |
| // Number of days to mute IPH after it's ignored for any app. |
| .global_mute_after_ignore_days = 14, |
| }; |
| |
| inline constexpr GuardrailPrefNames kIphPrefNames{ |
| // Pref key to store the last time IPH was ignored, stored in both app |
| // specific and app agnostic context. |
| .last_ignore_time_name = "IPH_last_ignore_time", |
| // Pref key to store the total number of ignores on the IPH bubble, stored |
| // in both app specific and app agnostic context. |
| .not_accepted_count_name = "IPH_num_of_consecutive_ignore", |
| // Pref key under which to store app agnostic IPH values. |
| .global_pref_name = ::prefs::kWebAppsAppAgnosticIphState, |
| }; |
| |
| // ----------------------ML guardrails---------------------------- |
| // Machine Learning (ML) triggered install prompts are limited by guardrails to |
| // avoid becoming a nuisance to users. This is an overview of how they work: |
| // - Accepting and installing an app from a prompt will not decrease further |
| // prompts and resets all guardrails. Otherwise: |
| // - Prompt is limited globally to one every: |
| // - 7 day if prompt is ignored. |
| // - 14 days if prompt is dismissed. |
| // - A specific site will only be suggested again after: |
| // - 14 days for an ignored prompt. |
| // - 28 days for a dismissed prompt. |
| // - For a specific site, the prompt is shown 3 times at max, and then it gets |
| // blocked. |
| // - Globally, the prompt is shown 5 times at max. |
| // - The guardrails are reset every `kTotalDaysToStoreMLGuardrails` days (this |
| // value is Finch configurable). |
| // - Example scenarios for triggering guardrails: |
| // - Multi site scenario: Visiting at least two ML promotable web-apps daily |
| // and ignoring the prompts. The prompt is then seen on days 0, 7, 14, 21 and |
| // 28, after which they are blocked. |
| // - Single site scenario: Visiting one ML promotable web-app daily and |
| // ignoring the prompts. The prompt is then seen on for the same app on day 0, |
| // 14 and 28, after which they are blocked. |
| // - In both cases, the user is blocked for `kTotalDaysToStoreMLGuardrails` |
| // days, after which the guardrails are cleared. |
| inline constexpr GuardrailData kMlPromoGuardrails{ |
| // Number of times ML triggered install dialog can be ignored for this app |
| // before it's muted. |
| .app_specific_not_accept_count = 3, |
| // Number of days to mute install dialog for this app after the ML triggered |
| // prompt was dismissed. |
| .app_specific_mute_after_dismiss_days = 28, |
| // Number of days to mute install dialog for this app after the ML triggered |
| // prompt was ignored. |
| .app_specific_mute_after_ignore_days = 14, |
| // Number of times ML triggered install dialog can be ignored for all apps |
| // before it's muted. |
| .global_not_accept_count = 5, |
| // Number of days to mute install dialog for any app after the ML triggered |
| // prompt was dismissed. |
| .global_mute_after_dismiss_days = 14, |
| // Number of days to mute install dialog for any app after the ML triggered |
| // prompt was ignored. |
| .global_mute_after_ignore_days = 7, |
| }; |
| |
| inline constexpr GuardrailPrefNames kMlPromoPrefNames{ |
| .last_ignore_time_name = "ML_last_time_install_ignored", |
| .last_dismiss_time_name = "ML_last_time_install_dismissed", |
| .not_accepted_count_name = "ML_num_of_consecutive_not_accepted", |
| .all_blocked_time_name = "ML_all_promos_blocked_date", |
| .global_pref_name = ::prefs::kWebAppsAppAgnosticMlState, |
| .block_reason_name = "ML_guardrail_blocked", |
| }; |
| |
| // -----------------------IPH Navigation Capturing guardrails------------------- |
| // Navigation capturing In Product Help (IPH) is limited by guardrails to avoid |
| // becoming a nuisance to users. This is an overview of how they work: |
| // - Accepting the IPH bubble will not decrease further prompts, and resets |
| // existing guardrails. All values are measured globally and not per app. |
| // - The IPH bubble is limited to 1 per day. |
| // - The IPH bubble shows up 6 times at max, after which it does not show up |
| // again. |
| // - Example scenarios for triggering guardrails: |
| // - User launches a site in an installed app with navigation capturing |
| // enabled and dismisses the IPH prompt. The prompt is then seen on days 0, 1, |
| // 2, 3, 4 and 5, after which the user never sees the IPH prompt again. |
| inline constexpr GuardrailData kIPHNavigationCapturingGuardrails{ |
| // Number of times IPH bubble can show up for any apps launched via |
| // navigation capturing before it's muted. |
| .global_not_accept_count = 6, |
| // Number of days to mute IPH for navigation captured app launches after |
| // it's dismissed for any app. |
| .global_mute_after_dismiss_days = 1, |
| }; |
| |
| // TODO(crbug.com/362123239): Rename pref keys from link capturing to navigation |
| // capturing, migrate data if needed. |
| inline constexpr GuardrailPrefNames kIPHNavigationCapturingPrefNames{ |
| .last_dismiss_time_name = "IPH_link_capturing_last_time_dismissed", |
| .not_accepted_count_name = |
| "IPH_link_capturing_consecutive_not_accepted_num", |
| .all_blocked_time_name = "IPH_link_capturing_blocked_date", |
| .global_pref_name = ::prefs::kWebAppsAppAgnosticIPHLinkCapturingState, |
| .block_reason_name = "IPH_link_capturing_block_reason", |
| }; |
| } // namespace web_app |
| |
| #endif // CHROME_BROWSER_WEB_APPLICATIONS_WEB_APP_PREF_GUARDRAILS_H_ |