blob: a095cec2a69f27c31fa9f561b4d70e64b3d62827 [file] [log] [blame]
// Copyright 2018 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 CHROME_BROWSER_UI_HATS_HATS_SERVICE_H_
#define CHROME_BROWSER_UI_HATS_HATS_SERVICE_H_
#include <memory>
#include <set>
#include <string>
#include "base/callback.h"
#include "base/callback_helpers.h"
#include "base/containers/flat_map.h"
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/time/time.h"
#include "components/keyed_service/core/keyed_service.h"
#include "content/public/browser/web_contents_observer.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace base {
struct Feature;
}
namespace content {
class WebContents;
}
namespace user_prefs {
class PrefRegistrySyncable;
}
class Browser;
class Profile;
// Trigger identifiers currently used; duplicates not allowed.
extern const char kHatsSurveyTriggerAccuracyTips[];
extern const char kHatsSurveyTriggerAutofillAddress[];
extern const char kHatsSurveyTriggerAutofillCard[];
extern const char kHatsSurveyTriggerAutofillPassword[];
extern const char kHatsSurveyTriggerDevToolsIssuesCOEP[];
extern const char kHatsSurveyTriggerDevToolsIssuesMixedContent[];
extern const char kHatsSurveyTriggerDevToolsIssuesCookiesSameSite[];
extern const char kHatsSurveyTriggerDevToolsIssuesHeavyAd[];
extern const char kHatsSurveyTriggerDevToolsIssuesCSP[];
extern const char kHatsSurveyTriggerJourneysHistoryEntrypoint[];
extern const char kHatsSurveyTriggerJourneysOmniboxEntrypoint[];
extern const char kHatsSurveyTriggerNtpModules[];
extern const char kHatsSurveyTriggerNtpPhotosModuleOptOut[];
extern const char kHatsSurveyTriggerPermissionsPostPrompt[];
extern const char kHatsSurveyTriggerPrivacyGuide[];
extern const char kHatsSurveyTriggerPrivacySandbox[];
extern const char kHatsSurveyTriggerSettings[];
extern const char kHatsSurveyTriggerSettingsPrivacy[];
extern const char kHatsSurveyTriggerTesting[];
extern const char kHatsSurveyTriggerTrustSafetyPrivacySandbox3ConsentAccept[];
extern const char kHatsSurveyTriggerTrustSafetyPrivacySandbox3ConsentDecline[];
extern const char kHatsSurveyTriggerTrustSafetyPrivacySandbox3NoticeDismiss[];
extern const char kHatsSurveyTriggerTrustSafetyPrivacySandbox3NoticeOk[];
extern const char kHatsSurveyTriggerTrustSafetyPrivacySandbox3NoticeSettings[];
extern const char kHatsSurveyTriggerTrustSafetyPrivacySandbox3NoticeLearnMore[];
extern const char kHatsSurveyTriggerTrustSafetyPrivacySettings[];
extern const char kHatsSurveyTriggerTrustSafetyTrustedSurface[];
extern const char kHatsSurveyTriggerTrustSafetyTransactions[];
extern const char kHatsSurveyTriggerWhatsNew[];
// The Trigger ID for a test HaTS Next survey which is available for testing
// and demo purposes when the migration feature flag is enabled.
extern const char kHatsNextSurveyTriggerIDTesting[];
// The name of the histogram which records if a survey was shown, or if not, the
// reason why not.
extern const char kHatsShouldShowSurveyReasonHistogram[];
// Key-value mapping type for survey's product specific bits data.
typedef std::map<std::string, bool> SurveyBitsData;
// Key-value mapping type for survey's product specific string data.
typedef std::map<std::string, std::string> SurveyStringData;
// This class provides the client side logic for determining if a
// survey should be shown for any trigger based on input from a finch
// configuration. It is created on a per profile basis.
class HatsService : public KeyedService {
public:
struct SurveyConfig {
// Constructs a SurveyConfig by inspecting |feature|. This includes checking
// if the feature is enabled, as well as inspecting the feature parameters
// for the survey probability, and if |presupplied_trigger_id| is not
// provided, the trigger ID. To pass any product specific data for the
// survey, configure fields here, matches are CHECK enforced.
SurveyConfig(
const base::Feature* feature,
const std::string& trigger,
const absl::optional<std::string>& presupplied_trigger_id =
absl::nullopt,
const std::vector<std::string>& product_specific_bits_data_fields = {},
const std::vector<std::string>& product_specific_string_data_fields =
{});
SurveyConfig();
SurveyConfig(const SurveyConfig&);
~SurveyConfig();
// Whether the survey is currently enabled and can be shown.
bool enabled = false;
// Probability [0,1] of how likely a chosen user will see the survey.
double probability = 0.0f;
// The trigger for this survey within the browser.
std::string trigger;
// Trigger ID for the survey.
std::string trigger_id;
// The survey will prompt every time because the user has explicitly decided
// to take the survey e.g. clicking a link.
bool user_prompted = false;
// Product Specific Bit Data fields which are sent with the survey
// response.
std::vector<std::string> product_specific_bits_data_fields;
// Product Specific String Data fields which are sent with the survey
// response.
std::vector<std::string> product_specific_string_data_fields;
};
struct SurveyMetadata {
SurveyMetadata();
~SurveyMetadata();
// Trigger specific metadata.
absl::optional<int> last_major_version;
absl::optional<base::Time> last_survey_started_time;
absl::optional<bool> is_survey_full;
absl::optional<base::Time> last_survey_check_time;
// Metadata affecting all triggers.
absl::optional<base::Time> any_last_survey_started_time;
};
class DelayedSurveyTask : public content::WebContentsObserver {
public:
DelayedSurveyTask(HatsService* hats_service,
const std::string& trigger,
content::WebContents* web_contents,
const SurveyBitsData& product_specific_bits_data,
const SurveyStringData& product_specific_string_data,
bool require_same_origin);
// Not copyable or movable
DelayedSurveyTask(const DelayedSurveyTask&) = delete;
DelayedSurveyTask& operator=(const DelayedSurveyTask&) = delete;
~DelayedSurveyTask() override;
// Asks |hats_service_| to launch the survey with id |trigger_| for tab
// |web_contents_|.
void Launch();
// content::WebContentsObserver
void DidFinishNavigation(
content::NavigationHandle* navigation_handle) override;
void WebContentsDestroyed() override;
// Returns a weak pointer to this object.
base::WeakPtr<DelayedSurveyTask> GetWeakPtr();
bool operator<(const HatsService::DelayedSurveyTask& other) const {
return trigger_ < other.trigger_ ? true
: web_contents() < other.web_contents();
}
private:
raw_ptr<HatsService> hats_service_;
std::string trigger_;
SurveyBitsData product_specific_bits_data_;
SurveyStringData product_specific_string_data_;
bool require_same_origin_;
base::WeakPtrFactory<DelayedSurveyTask> weak_ptr_factory_{this};
};
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class ShouldShowSurveyReasons {
kYes = 0,
kNoOffline = 1,
kNoLastSessionCrashed = 2,
kNoReceivedSurveyInCurrentMilestone = 3,
kNoProfileTooNew = 4,
kNoLastSurveyTooRecent = 5,
kNoBelowProbabilityLimit = 6,
kNoTriggerStringMismatch = 7,
kNoNotRegularBrowser = 8,
kNoIncognitoDisabled = 9,
kNoCookiesBlocked = 10, // Unused.
kNoThirdPartyCookiesBlocked = 11, // Unused.
kNoSurveyUnreachable = 12,
kNoSurveyOverCapacity = 13,
kNoSurveyAlreadyInProgress = 14,
kNoAnyLastSurveyTooRecent = 15,
kNoRejectedByHatsService = 16,
kMaxValue = kNoRejectedByHatsService,
};
explicit HatsService(Profile* profile);
HatsService(const HatsService&) = delete;
HatsService& operator=(const HatsService&) = delete;
~HatsService() override;
static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
// Launches survey with identifier |trigger| if appropriate.
// |success_callback| is called when the survey is shown to the user.
// |failure_callback| is called if the survey does not launch for any reason.
// |product_specific_bits_data| and |product_specific_string_data| must
// contain key-value pairs where the keys match the field names set for the
// survey in hats_service.cc, and the values are those which will be
// associated with the survey response. Field's matches are CHECK enforced.
virtual void LaunchSurvey(
const std::string& trigger,
base::OnceClosure success_callback = base::DoNothing(),
base::OnceClosure failure_callback = base::DoNothing(),
const SurveyBitsData& product_specific_bits_data = {},
const SurveyStringData& product_specific_string_data = {});
// Launches survey (with id |trigger|) with a timeout |timeout_ms| if
// appropriate. Survey will be shown at the active window/tab by the
// time of launching. Rejects (and returns false) if the underlying task
// posting fails.
virtual bool LaunchDelayedSurvey(
const std::string& trigger,
int timeout_ms,
const SurveyBitsData& product_specific_bits_data = {},
const SurveyStringData& product_specific_string_data = {});
// Launches survey (with id |trigger|) with a timeout |timeout_ms| for tab
// |web_contents| if appropriate. |web_contents| required to be non-nullptr.
// Launch is cancelled if |web_contents| killed before end of timeout. Launch
// is also cancelled if |web_contents| not visible at the time of launch.
// Rejects (and returns false) if there is already an identical delayed-task
// (same |trigger| and same |web_contents|) waiting to be fulfilled. Also
// rejects if the underlying task posting fails. If |require_same_origin| is
// set, additionally requires that |web_contents| remain on the same origin.
virtual bool LaunchDelayedSurveyForWebContents(
const std::string& trigger,
content::WebContents* web_contents,
int timeout_ms,
const SurveyBitsData& product_specific_bits_data = {},
const SurveyStringData& product_specific_string_data = {},
bool require_same_origin = false);
// Updates the user preferences to record that the survey associated with
// |survey_id| was shown to the user. |trigger_id| is the HaTS next Trigger
// ID for the survey.
void RecordSurveyAsShown(std::string trigger_id);
// Indicates to the service that the HaTS Next dialog has been closed.
// Virtual to allow mocking in tests.
virtual void HatsNextDialogClosed();
void SetSurveyMetadataForTesting(const SurveyMetadata& metadata);
void GetSurveyMetadataForTesting(HatsService::SurveyMetadata* metadata) const;
bool HasPendingTasks();
// Whether the survey specified by |trigger| can be shown to the user. This
// is a pre-check that calculates as many conditions as possible, but could
// still return a false positive due to client-side rate limiting, a change
// in network conditions, or intervening calls to this API.
bool CanShowSurvey(const std::string& trigger) const;
// Whether the user is eligible for any survey (of the type |user_prompted|
// or not) to be shown. A return value of false is always a true-negative, and
// means the user is currently ineligible for all surveys. A return value of
// true should not be interpreted as a guarantee that requests to show a
// survey will succeed. Virtual to allow mocking in tests.
virtual bool CanShowAnySurvey(bool user_prompted) const;
// Returns whether a HaTS Next dialog currently exists, regardless of whether
// it is being shown or not.
bool hats_next_dialog_exists_for_testing() {
return hats_next_dialog_exists_;
}
private:
friend class DelayedSurveyTask;
FRIEND_TEST_ALL_PREFIXES(HatsServiceProbabilityOne, SingleHatsNextDialog);
void LaunchSurveyForWebContents(
const std::string& trigger,
content::WebContents* web_contents,
const SurveyBitsData& product_specific_bits_data,
const SurveyStringData& product_specific_string_data);
void LaunchSurveyForBrowser(
Browser* browser,
const std::string& trigger,
base::OnceClosure success_callback,
base::OnceClosure failure_callback,
const SurveyBitsData& product_specific_bits_data,
const SurveyStringData& product_specific_string_data);
// Returns true is the survey trigger specified should be shown.
bool ShouldShowSurvey(const std::string& trigger) const;
// Check whether the survey is reachable and under capacity and show it.
// |success_callback| is called when the survey is shown to the user.
// |failure_callback| is called if the survey does not launch for any reason.
// The matches of field names with the `SurveyConfig` are CHECK enforced.
void CheckSurveyStatusAndMaybeShow(
Browser* browser,
const std::string& trigger,
base::OnceClosure success_callback,
base::OnceClosure failure_callback,
const SurveyBitsData& product_specific_bits_data,
const SurveyStringData& product_specific_string_data);
// Remove |task| from the set of |pending_tasks_|.
void RemoveTask(const DelayedSurveyTask& task);
// Profile associated with this service.
const raw_ptr<Profile> profile_;
std::set<DelayedSurveyTask> pending_tasks_;
base::flat_map<std::string, SurveyConfig> survey_configs_by_triggers_;
// Whether a HaTS Next dialog currently exists (regardless of whether it
// is being shown to the user).
bool hats_next_dialog_exists_ = false;
base::WeakPtrFactory<HatsService> weak_ptr_factory_{this};
};
#endif // CHROME_BROWSER_UI_HATS_HATS_SERVICE_H_