| // 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/password_manager/core/browser/password_form_metrics_recorder.h" |
| |
| #include <algorithm> |
| |
| #include "base/logging.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "components/password_manager/core/browser/form_fetcher.h" |
| #include "components/password_manager/core/browser/password_manager_metrics_util.h" |
| |
| using autofill::PasswordForm; |
| |
| namespace password_manager { |
| |
| namespace { |
| |
| PasswordFormMetricsRecorder::BubbleDismissalReason GetBubbleDismissalReason( |
| metrics_util::UIDismissalReason ui_dismissal_reason) { |
| using BubbleDismissalReason = |
| PasswordFormMetricsRecorder::BubbleDismissalReason; |
| switch (ui_dismissal_reason) { |
| // Accepted by user. |
| case metrics_util::CLICKED_SAVE: |
| return BubbleDismissalReason::kAccepted; |
| |
| // Declined by user. |
| case metrics_util::CLICKED_CANCEL: |
| case metrics_util::CLICKED_NEVER: |
| return BubbleDismissalReason::kDeclined; |
| |
| // Ignored by user. |
| case metrics_util::NO_DIRECT_INTERACTION: |
| return BubbleDismissalReason::kIgnored; |
| |
| // Ignore these for metrics collection: |
| case metrics_util::CLICKED_MANAGE: |
| case metrics_util::CLICKED_PASSWORDS_DASHBOARD: |
| case metrics_util::AUTO_SIGNIN_TOAST_TIMEOUT: |
| break; |
| |
| // These should not reach here: |
| case metrics_util::CLICKED_DONE_OBSOLETE: |
| case metrics_util::CLICKED_OK_OBSOLETE: |
| case metrics_util::CLICKED_UNBLACKLIST_OBSOLETE: |
| case metrics_util::CLICKED_CREDENTIAL_OBSOLETE: |
| case metrics_util::AUTO_SIGNIN_TOAST_CLICKED_OBSOLETE: |
| case metrics_util::CLICKED_BRAND_NAME_OBSOLETE: |
| case metrics_util::NUM_UI_RESPONSES: |
| NOTREACHED(); |
| break; |
| } |
| return BubbleDismissalReason::kUnknown; |
| } |
| |
| } // namespace |
| |
| PasswordFormMetricsRecorder::PasswordFormMetricsRecorder( |
| bool is_main_frame_secure, |
| ukm::SourceId source_id) |
| : is_main_frame_secure_(is_main_frame_secure), |
| source_id_(source_id), |
| ukm_entry_builder_(source_id) {} |
| |
| PasswordFormMetricsRecorder::~PasswordFormMetricsRecorder() { |
| UMA_HISTOGRAM_ENUMERATION("PasswordManager.ActionsTakenV3", GetActionsTaken(), |
| kMaxNumActionsTaken); |
| ukm_entry_builder_.SetUser_ActionSimplified( |
| static_cast<int64_t>(user_action_)); |
| |
| // Use the visible main frame URL at the time the PasswordFormManager |
| // is created, in case a navigation has already started and the |
| // visible URL has changed. |
| if (!is_main_frame_secure_) { |
| UMA_HISTOGRAM_ENUMERATION("PasswordManager.ActionsTakenOnNonSecureForm", |
| GetActionsTaken(), kMaxNumActionsTaken); |
| } |
| |
| if (submit_result_ == kSubmitResultNotSubmitted) { |
| if (has_generated_password_) { |
| metrics_util::LogPasswordGenerationSubmissionEvent( |
| metrics_util::PASSWORD_NOT_SUBMITTED); |
| } else if (generation_available_) { |
| metrics_util::LogPasswordGenerationAvailableSubmissionEvent( |
| metrics_util::PASSWORD_NOT_SUBMITTED); |
| } |
| ukm_entry_builder_.SetSubmission_Observed(0 /*false*/); |
| } |
| |
| if (submitted_form_type_ != kSubmittedFormTypeUnspecified) { |
| UMA_HISTOGRAM_ENUMERATION("PasswordManager.SubmittedFormType", |
| submitted_form_type_, kSubmittedFormTypeMax); |
| if (!is_main_frame_secure_) { |
| UMA_HISTOGRAM_ENUMERATION("PasswordManager.SubmittedNonSecureFormType", |
| submitted_form_type_, kSubmittedFormTypeMax); |
| } |
| |
| ukm_entry_builder_.SetSubmission_SubmittedFormType(submitted_form_type_); |
| } |
| |
| ukm_entry_builder_.SetUpdating_Prompt_Shown(update_prompt_shown_); |
| ukm_entry_builder_.SetSaving_Prompt_Shown(save_prompt_shown_); |
| |
| for (const auto& action : detailed_user_actions_counts_) { |
| switch (action.first) { |
| case DetailedUserAction::kEditedUsernameInBubble: |
| ukm_entry_builder_.SetUser_Action_EditedUsernameInBubble(action.second); |
| break; |
| case DetailedUserAction::kSelectedDifferentPasswordInBubble: |
| ukm_entry_builder_.SetUser_Action_SelectedDifferentPasswordInBubble( |
| action.second); |
| break; |
| case DetailedUserAction::kTriggeredManualFallbackForSaving: |
| ukm_entry_builder_.SetUser_Action_TriggeredManualFallbackForSaving( |
| action.second); |
| break; |
| case DetailedUserAction::kCorrectedUsernameInForm: |
| ukm_entry_builder_.SetUser_Action_CorrectedUsernameInForm( |
| action.second); |
| break; |
| case DetailedUserAction::kObsoleteTriggeredManualFallbackForUpdating: |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| ukm_entry_builder_.SetGeneration_GeneratedPassword( |
| has_generated_password_ ? 1 : 0); |
| if (has_generated_password_) { |
| ukm_entry_builder_.SetGeneration_GeneratedPasswordModified( |
| has_generated_password_changed_ ? 1 : 0); |
| } |
| if (password_generation_popup_shown_ != |
| PasswordGenerationPopupShown::kNotShown) { |
| ukm_entry_builder_.SetGeneration_PopupShown( |
| static_cast<int64_t>(password_generation_popup_shown_)); |
| } |
| if (spec_priority_of_generated_password_) { |
| ukm_entry_builder_.SetGeneration_SpecPriority( |
| spec_priority_of_generated_password_.value()); |
| } |
| |
| if (showed_manual_fallback_for_saving_) { |
| ukm_entry_builder_.SetSaving_ShowedManualFallbackForSaving( |
| showed_manual_fallback_for_saving_.value()); |
| } |
| |
| ukm_entry_builder_.Record(ukm::UkmRecorder::Get()); |
| } |
| |
| void PasswordFormMetricsRecorder::MarkGenerationAvailable() { |
| generation_available_ = true; |
| } |
| |
| void PasswordFormMetricsRecorder::SetHasGeneratedPassword( |
| bool has_generated_password) { |
| has_generated_password_ = has_generated_password; |
| } |
| |
| void PasswordFormMetricsRecorder::SetHasGeneratedPasswordChanged( |
| bool has_generated_password_changed) { |
| has_generated_password_changed_ = has_generated_password_changed; |
| } |
| |
| void PasswordFormMetricsRecorder::ReportSpecPriorityForGeneratedPassword( |
| uint32_t spec_priority) { |
| spec_priority_of_generated_password_ = spec_priority; |
| } |
| |
| void PasswordFormMetricsRecorder::SetManagerAction( |
| ManagerAction manager_action) { |
| manager_action_ = manager_action; |
| } |
| |
| void PasswordFormMetricsRecorder::SetUserAction(UserAction user_action) { |
| if (user_action == UserAction::kChoose) { |
| base::RecordAction( |
| base::UserMetricsAction("PasswordManager_UsedNonDefaultUsername")); |
| } else if (user_action == UserAction::kChoosePslMatch) { |
| base::RecordAction( |
| base::UserMetricsAction("PasswordManager_ChoseSubdomainPassword")); |
| } else if (user_action == UserAction::kOverridePassword) { |
| base::RecordAction( |
| base::UserMetricsAction("PasswordManager_LoggedInWithNewPassword")); |
| } else if (user_action == UserAction::kOverrideUsernameAndPassword) { |
| base::RecordAction( |
| base::UserMetricsAction("PasswordManager_LoggedInWithNewUsername")); |
| } else { |
| NOTREACHED(); |
| } |
| user_action_ = user_action; |
| } |
| |
| void PasswordFormMetricsRecorder::LogSubmitPassed() { |
| if (submit_result_ != kSubmitResultFailed) { |
| if (has_generated_password_) { |
| metrics_util::LogPasswordGenerationSubmissionEvent( |
| metrics_util::PASSWORD_SUBMITTED); |
| } else if (generation_available_) { |
| metrics_util::LogPasswordGenerationAvailableSubmissionEvent( |
| metrics_util::PASSWORD_SUBMITTED); |
| } |
| } |
| base::RecordAction(base::UserMetricsAction("PasswordManager_LoginPassed")); |
| ukm_entry_builder_.SetSubmission_Observed(1 /*true*/); |
| ukm_entry_builder_.SetSubmission_SubmissionResult(kSubmitResultPassed); |
| submit_result_ = kSubmitResultPassed; |
| } |
| |
| void PasswordFormMetricsRecorder::LogSubmitFailed() { |
| if (has_generated_password_) { |
| metrics_util::LogPasswordGenerationSubmissionEvent( |
| metrics_util::GENERATED_PASSWORD_FORCE_SAVED); |
| } else if (generation_available_) { |
| metrics_util::LogPasswordGenerationAvailableSubmissionEvent( |
| metrics_util::PASSWORD_SUBMISSION_FAILED); |
| } |
| base::RecordAction(base::UserMetricsAction("PasswordManager_LoginFailed")); |
| ukm_entry_builder_.SetSubmission_Observed(1 /*true*/); |
| ukm_entry_builder_.SetSubmission_SubmissionResult(kSubmitResultFailed); |
| submit_result_ = kSubmitResultFailed; |
| } |
| |
| void PasswordFormMetricsRecorder::SetPasswordGenerationPopupShown( |
| bool generation_popup_was_shown, |
| bool is_manual_generation) { |
| password_generation_popup_shown_ = |
| generation_popup_was_shown |
| ? (is_manual_generation |
| ? PasswordGenerationPopupShown::kShownManually |
| : PasswordGenerationPopupShown::kShownAutomatically) |
| : PasswordGenerationPopupShown::kNotShown; |
| } |
| |
| void PasswordFormMetricsRecorder::SetSubmittedFormType( |
| SubmittedFormType form_type) { |
| submitted_form_type_ = form_type; |
| } |
| |
| void PasswordFormMetricsRecorder::SetSubmissionIndicatorEvent( |
| autofill::PasswordForm::SubmissionIndicatorEvent event) { |
| ukm_entry_builder_.SetSubmission_Indicator(static_cast<int>(event)); |
| } |
| |
| int PasswordFormMetricsRecorder::GetActionsTakenNew() const { |
| // Merge kManagerActionNone and kManagerActionBlacklisted_Obsolete. This |
| // lowers the number of histogram buckets used by 33%. |
| ManagerActionNew manager_action_new = |
| (manager_action_ == kManagerActionAutofilled) |
| ? kManagerActionNewAutofilled |
| : kManagerActionNewNone; |
| |
| return static_cast<int>(user_action_) + |
| static_cast<int>(UserAction::kMax) * |
| (manager_action_new + kManagerActionNewMax * submit_result_); |
| } |
| |
| void PasswordFormMetricsRecorder::RecordDetailedUserAction( |
| PasswordFormMetricsRecorder::DetailedUserAction action) { |
| detailed_user_actions_counts_[action]++; |
| } |
| |
| // static |
| int64_t PasswordFormMetricsRecorder::HashFormSignature( |
| autofill::FormSignature form_signature) { |
| // Note that this is an intentionally small hash domain for privacy reasons. |
| return static_cast<uint64_t>(form_signature) % 1021; |
| } |
| |
| void PasswordFormMetricsRecorder::RecordFormSignature( |
| autofill::FormSignature form_signature) { |
| ukm_entry_builder_.SetContext_FormSignature( |
| HashFormSignature(form_signature)); |
| } |
| |
| void PasswordFormMetricsRecorder::RecordParsingsComparisonResult( |
| ParsingComparisonResult comparison_result) { |
| ukm_entry_builder_.SetParsingComparison( |
| static_cast<uint64_t>(comparison_result)); |
| } |
| |
| void PasswordFormMetricsRecorder::RecordParsingOnSavingDifference( |
| uint64_t comparison_result) { |
| ukm_entry_builder_.SetParsingOnSavingDifference(comparison_result); |
| } |
| |
| void PasswordFormMetricsRecorder::RecordReadonlyWhenFilling(uint64_t value) { |
| ukm_entry_builder_.SetReadonlyWhenFilling(value); |
| } |
| |
| void PasswordFormMetricsRecorder::RecordReadonlyWhenSaving(uint64_t value) { |
| ukm_entry_builder_.SetReadonlyWhenSaving(value); |
| } |
| |
| void PasswordFormMetricsRecorder::RecordShowManualFallbackForSaving( |
| bool has_generated_password, |
| bool is_update) { |
| showed_manual_fallback_for_saving_ = |
| 1 + (has_generated_password ? 2 : 0) + (is_update ? 4 : 0); |
| } |
| |
| int PasswordFormMetricsRecorder::GetActionsTaken() const { |
| return static_cast<int>(user_action_) + |
| static_cast<int>(UserAction::kMax) * |
| (manager_action_ + kManagerActionMax * submit_result_); |
| } |
| |
| void PasswordFormMetricsRecorder::RecordHistogramsOnSuppressedAccounts( |
| bool observed_form_origin_has_cryptographic_scheme, |
| const FormFetcher& form_fetcher, |
| const PasswordForm& pending_credentials) { |
| UMA_HISTOGRAM_BOOLEAN("PasswordManager.QueryingSuppressedAccountsFinished", |
| form_fetcher.DidCompleteQueryingSuppressedForms()); |
| |
| if (!form_fetcher.DidCompleteQueryingSuppressedForms()) |
| return; |
| |
| SuppressedAccountExistence best_match = kSuppressedAccountNone; |
| |
| if (!observed_form_origin_has_cryptographic_scheme) { |
| best_match = GetBestMatchingSuppressedAccount( |
| form_fetcher.GetSuppressedHTTPSForms(), PasswordForm::TYPE_GENERATED, |
| pending_credentials); |
| UMA_HISTOGRAM_ENUMERATION( |
| "PasswordManager.SuppressedAccount.Generated.HTTPSNotHTTP", |
| GetHistogramSampleForSuppressedAccounts(best_match), |
| kMaxSuppressedAccountStats); |
| ukm_entry_builder_.SetSuppressedAccount_Generated_HTTPSNotHTTP(best_match); |
| |
| best_match = GetBestMatchingSuppressedAccount( |
| form_fetcher.GetSuppressedHTTPSForms(), PasswordForm::TYPE_MANUAL, |
| pending_credentials); |
| UMA_HISTOGRAM_ENUMERATION( |
| "PasswordManager.SuppressedAccount.Manual.HTTPSNotHTTP", |
| GetHistogramSampleForSuppressedAccounts(best_match), |
| kMaxSuppressedAccountStats); |
| ukm_entry_builder_.SetSuppressedAccount_Manual_HTTPSNotHTTP(best_match); |
| } |
| |
| best_match = GetBestMatchingSuppressedAccount( |
| form_fetcher.GetSuppressedPSLMatchingForms(), |
| PasswordForm::TYPE_GENERATED, pending_credentials); |
| UMA_HISTOGRAM_ENUMERATION( |
| "PasswordManager.SuppressedAccount.Generated.PSLMatching", |
| GetHistogramSampleForSuppressedAccounts(best_match), |
| kMaxSuppressedAccountStats); |
| ukm_entry_builder_.SetSuppressedAccount_Generated_PSLMatching(best_match); |
| best_match = GetBestMatchingSuppressedAccount( |
| form_fetcher.GetSuppressedPSLMatchingForms(), PasswordForm::TYPE_MANUAL, |
| pending_credentials); |
| UMA_HISTOGRAM_ENUMERATION( |
| "PasswordManager.SuppressedAccount.Manual.PSLMatching", |
| GetHistogramSampleForSuppressedAccounts(best_match), |
| kMaxSuppressedAccountStats); |
| ukm_entry_builder_.SetSuppressedAccount_Manual_PSLMatching(best_match); |
| |
| best_match = GetBestMatchingSuppressedAccount( |
| form_fetcher.GetSuppressedSameOrganizationNameForms(), |
| PasswordForm::TYPE_GENERATED, pending_credentials); |
| UMA_HISTOGRAM_ENUMERATION( |
| "PasswordManager.SuppressedAccount.Generated.SameOrganizationName", |
| GetHistogramSampleForSuppressedAccounts(best_match), |
| kMaxSuppressedAccountStats); |
| ukm_entry_builder_.SetSuppressedAccount_Generated_SameOrganizationName( |
| best_match); |
| best_match = GetBestMatchingSuppressedAccount( |
| form_fetcher.GetSuppressedSameOrganizationNameForms(), |
| PasswordForm::TYPE_MANUAL, pending_credentials); |
| UMA_HISTOGRAM_ENUMERATION( |
| "PasswordManager.SuppressedAccount.Manual.SameOrganizationName", |
| GetHistogramSampleForSuppressedAccounts(best_match), |
| kMaxSuppressedAccountStats); |
| ukm_entry_builder_.SetSuppressedAccount_Manual_SameOrganizationName( |
| best_match); |
| |
| if (current_bubble_ != CurrentBubbleOfInterest::kNone) |
| RecordUIDismissalReason(metrics_util::NO_DIRECT_INTERACTION); |
| } |
| |
| void PasswordFormMetricsRecorder::RecordPasswordBubbleShown( |
| metrics_util::CredentialSourceType credential_source_type, |
| metrics_util::UIDisplayDisposition display_disposition) { |
| if (credential_source_type == metrics_util::CredentialSourceType::kUnknown) |
| return; |
| DCHECK_EQ(CurrentBubbleOfInterest::kNone, current_bubble_); |
| BubbleTrigger automatic_trigger_type = |
| credential_source_type == |
| metrics_util::CredentialSourceType::kPasswordManager |
| ? BubbleTrigger::kPasswordManagerSuggestionAutomatic |
| : BubbleTrigger::kCredentialManagementAPIAutomatic; |
| BubbleTrigger manual_trigger_type = |
| credential_source_type == |
| metrics_util::CredentialSourceType::kPasswordManager |
| ? BubbleTrigger::kPasswordManagerSuggestionManual |
| : BubbleTrigger::kCredentialManagementAPIManual; |
| |
| switch (display_disposition) { |
| // New credential cases: |
| case metrics_util::AUTOMATIC_WITH_PASSWORD_PENDING: |
| current_bubble_ = CurrentBubbleOfInterest::kSaveBubble; |
| save_prompt_shown_ = true; |
| ukm_entry_builder_.SetSaving_Prompt_Trigger( |
| static_cast<int64_t>(automatic_trigger_type)); |
| break; |
| case metrics_util::MANUAL_WITH_PASSWORD_PENDING: |
| current_bubble_ = CurrentBubbleOfInterest::kSaveBubble; |
| save_prompt_shown_ = true; |
| ukm_entry_builder_.SetSaving_Prompt_Trigger( |
| static_cast<int64_t>(manual_trigger_type)); |
| break; |
| |
| // Update cases: |
| case metrics_util::AUTOMATIC_WITH_PASSWORD_PENDING_UPDATE: |
| current_bubble_ = CurrentBubbleOfInterest::kUpdateBubble; |
| update_prompt_shown_ = true; |
| ukm_entry_builder_.SetUpdating_Prompt_Trigger( |
| static_cast<int64_t>(automatic_trigger_type)); |
| break; |
| case metrics_util::MANUAL_WITH_PASSWORD_PENDING_UPDATE: |
| current_bubble_ = CurrentBubbleOfInterest::kUpdateBubble; |
| update_prompt_shown_ = true; |
| ukm_entry_builder_.SetUpdating_Prompt_Trigger( |
| static_cast<int64_t>(manual_trigger_type)); |
| break; |
| |
| // Other reasons to show a bubble: |
| case metrics_util::MANUAL_MANAGE_PASSWORDS: |
| case metrics_util::AUTOMATIC_GENERATED_PASSWORD_CONFIRMATION: |
| case metrics_util::MANUAL_GENERATED_PASSWORD_CONFIRMATION: |
| case metrics_util::AUTOMATIC_SIGNIN_TOAST: |
| // Do nothing. |
| return; |
| |
| // Obsolte display dispositions: |
| case metrics_util::MANUAL_BLACKLISTED_OBSOLETE: |
| case metrics_util::AUTOMATIC_CREDENTIAL_REQUEST_OBSOLETE: |
| case metrics_util::NUM_DISPLAY_DISPOSITIONS: |
| NOTREACHED(); |
| return; |
| } |
| } |
| |
| void PasswordFormMetricsRecorder::RecordUIDismissalReason( |
| metrics_util::UIDismissalReason ui_dismissal_reason) { |
| if (current_bubble_ != CurrentBubbleOfInterest::kUpdateBubble && |
| current_bubble_ != CurrentBubbleOfInterest::kSaveBubble) |
| return; |
| auto bubble_dismissal_reason = GetBubbleDismissalReason(ui_dismissal_reason); |
| if (bubble_dismissal_reason != BubbleDismissalReason::kUnknown) { |
| if (current_bubble_ == CurrentBubbleOfInterest::kUpdateBubble) { |
| ukm_entry_builder_.SetUpdating_Prompt_Interaction( |
| static_cast<int64_t>(bubble_dismissal_reason)); |
| } else { |
| ukm_entry_builder_.SetSaving_Prompt_Interaction( |
| static_cast<int64_t>(bubble_dismissal_reason)); |
| } |
| } |
| |
| current_bubble_ = CurrentBubbleOfInterest::kNone; |
| } |
| |
| void PasswordFormMetricsRecorder::RecordFillEvent(ManagerAutofillEvent event) { |
| ukm_entry_builder_.SetManagerFill_Action(event); |
| } |
| |
| PasswordFormMetricsRecorder::SuppressedAccountExistence |
| PasswordFormMetricsRecorder::GetBestMatchingSuppressedAccount( |
| const std::vector<const PasswordForm*>& suppressed_forms, |
| PasswordForm::Type manual_or_generated, |
| const PasswordForm& pending_credentials) const { |
| SuppressedAccountExistence best_matching_account = kSuppressedAccountNone; |
| for (const PasswordForm* form : suppressed_forms) { |
| if (form->type != manual_or_generated) |
| continue; |
| |
| SuppressedAccountExistence current_account; |
| if (pending_credentials.password_value.empty()) |
| current_account = kSuppressedAccountExists; |
| else if (form->username_value != pending_credentials.username_value) |
| current_account = kSuppressedAccountExistsDifferentUsername; |
| else if (form->password_value != pending_credentials.password_value) |
| current_account = kSuppressedAccountExistsSameUsername; |
| else |
| current_account = kSuppressedAccountExistsSameUsernameAndPassword; |
| |
| best_matching_account = std::max(best_matching_account, current_account); |
| } |
| return best_matching_account; |
| } |
| |
| int PasswordFormMetricsRecorder::GetHistogramSampleForSuppressedAccounts( |
| SuppressedAccountExistence best_matching_account) const { |
| // Encoding: most significant digit is the |best_matching_account|. |
| int mixed_base_encoding = 0; |
| mixed_base_encoding += best_matching_account; |
| mixed_base_encoding *= PasswordFormMetricsRecorder::kMaxNumActionsTakenNew; |
| mixed_base_encoding += GetActionsTakenNew(); |
| DCHECK_LT(mixed_base_encoding, kMaxSuppressedAccountStats); |
| return mixed_base_encoding; |
| } |
| |
| } // namespace password_manager |