From 3e26e12c14b72b7a5830aec8ba8ac478203b3c7f Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Thu, 1 Jun 2023 09:27:11 -0700 Subject: [PATCH 01/20] Sort analytics events (#8519) * Remove a couple of stray extra lines in analytics_events.rb * Sort analytics_events.rb * Check analytics events are sorted during lint changelog: Internal, analytics, Sort analytics_events.rb and enforce sorting in CI * Add important notice to the top of the file * Purposefully sort analytics events incorrectly to test CI it occurs to me that i should ensure the combo of make / sh / grep on the ci runner delivers the same behavior as it does locally * Revert "Purposefully sort analytics events incorrectly to test CI" This reverts commit c29940c79c74c6673c050d46be0f0713ecd2cff8. --- Makefile | 7 + app/services/analytics_events.rb | 5050 +++++++++++++++--------------- 2 files changed, 2536 insertions(+), 2521 deletions(-) diff --git a/Makefile b/Makefile index 08925bfd1fa..4de4b18ca2d 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ ARTIFACT_DESTINATION_FILE ?= ./tmp/idp.tar.gz help \ lint \ lint_analytics_events \ + lint_analytics_events_sorted \ lint_tracker_events \ lint_country_dialing_codes \ lint_erb \ @@ -75,6 +76,7 @@ endif @echo "--- analytics_events ---" make lint_analytics_events make lint_tracker_events + make lint_analytics_events_sorted @echo "--- brakeman ---" bundle exec brakeman @echo "--- bundler-audit ---" @@ -102,6 +104,7 @@ endif @echo "--- lint migrations ---" make lint_migrations + lint_erb: ## Lints ERB files bundle exec erblint app/views app/components @@ -247,6 +250,10 @@ analytics_events: public/api/_analytics-events.json ## Generates a JSON file tha lint_analytics_events: .yardoc ## Checks that all methods on AnalyticsEvents are documented bundle exec ruby lib/analytics_events_documenter.rb --class-name="AnalyticsEvents" --check $< +lint_analytics_events_sorted: + @test "$(shell grep '^ def ' app/services/analytics_events.rb)" = "$(shell grep '^ def ' app/services/analytics_events.rb | sort)" \ + || (echo 'Error: methods in analytics_events.rb are not sorted alphabetically' && exit 1) + lint_tracker_events: .yardoc ## Checks that all methods on AnalyticsEvents are documented bundle exec ruby lib/analytics_events_documenter.rb --class-name="IrsAttemptsApi::TrackerEvents" --check --skip-extra-params $< diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 457a6dd8406..4de7b56937e 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -1,6 +1,33 @@ # frozen_string_literal: true +# ______________________________________ +# / Adding something new in here? Please \ +# \ keep methods sorted alphabetically. / +# -------------------------------------- +# \ ^__^ +# \ (oo)\_______ +# (__)\ )\/\ +# ||----w | +# || || + module AnalyticsEvents + # @param [Boolean] success + # When a user submits a form to delete their account + def account_delete_submitted(success:, **extra) + track_event('Account Delete submitted', success: success, **extra) + end + + # When a user visits the page to delete their account + def account_delete_visited + track_event('Account Delete visited') + end + + # @param [String] request_came_from the controller/action the request came from + # When a user deletes their account + def account_deletion(request_came_from:, **extra) + track_event('Account Deletion Requested', request_came_from: request_came_from, **extra) + end + # @identity.idp.previous_event_name Account Reset # @param [String] user_id # @param [String, nil] message_id from AWS Pinpoint API @@ -18,6 +45,19 @@ def account_reset_cancel(user_id:, message_id: nil, request_id: nil, **extra) ) end + # @identity.idp.previous_event_name Account Reset + # @param [String] user_id + # @param [Hash] errors + # Validates the token used for cancelling an account reset + def account_reset_cancel_token_validation(user_id:, errors: nil, **extra) + track_event( + 'Account Reset: cancel token validation', + user_id: user_id, + errors: errors, + **extra, + ) + end + # @identity.idp.previous_event_name Account Reset # @param [Boolean] success # @param [String] user_id @@ -45,6 +85,31 @@ def account_reset_delete( ) end + # @identity.idp.previous_event_name Account Reset + # @param [String] user_id + # @param [Hash] errors + # Validates the granted token for account reset + def account_reset_granted_token_validation(user_id: nil, errors: nil, **extra) + track_event( + 'Account Reset: granted token validation', + user_id: user_id, + errors: errors, + **extra, + ) + end + + # @identity.idp.previous_event_name Account Reset + # @param [Integer] count number of email notifications sent + # Account reset was performed, logs the number of email notifications sent + def account_reset_notifications(count:, **extra) + track_event('Account Reset: notifications', count: count, **extra) + end + + # Tracks users visiting the recovery options page + def account_reset_recovery_options_visit + track_event('Account Reset: Recovery Options Visited') + end + # @identity.idp.previous_event_name Account Reset # @param [Boolean] success # @param [Boolean] sms_phone does the user have a phone factor configured? @@ -79,61 +144,11 @@ def account_reset_request( ) end - # @identity.idp.previous_event_name Account Reset - # @param [String] user_id - # @param [Hash] errors - # Validates the token used for cancelling an account reset - def account_reset_cancel_token_validation(user_id:, errors: nil, **extra) - track_event( - 'Account Reset: cancel token validation', - user_id: user_id, - errors: errors, - **extra, - ) - end - - # @identity.idp.previous_event_name Account Reset - # @param [String] user_id - # @param [Hash] errors - # Validates the granted token for account reset - def account_reset_granted_token_validation(user_id: nil, errors: nil, **extra) - track_event( - 'Account Reset: granted token validation', - user_id: user_id, - errors: errors, - **extra, - ) - end - - # @identity.idp.previous_event_name Account Reset - # @param [Integer] count number of email notifications sent - # Account reset was performed, logs the number of email notifications sent - def account_reset_notifications(count:, **extra) - track_event('Account Reset: notifications', count: count, **extra) - end - # User visited the account deletion and reset page def account_reset_visit track_event('Account deletion and reset visited') end - # @param [Boolean] success - # When a user submits a form to delete their account - def account_delete_submitted(success:, **extra) - track_event('Account Delete submitted', success: success, **extra) - end - - # When a user visits the page to delete their account - def account_delete_visited - track_event('Account Delete visited') - end - - # @param [String] request_came_from the controller/action the request came from - # When a user deletes their account - def account_deletion(request_came_from:, **extra) - track_event('Account Deletion Requested', request_came_from: request_came_from, **extra) - end - # When a user views the account page def account_visit track_event('Account Page Visited') @@ -146,6 +161,25 @@ def add_email_confirmation(user_id:, success: nil, **extra) track_event('Add Email: Email Confirmation', user_id: user_id, success: success, **extra) end + # @param [Boolean] success + # @param [Hash] errors + # Tracks request for adding new emails to an account + def add_email_request(success:, errors:, **extra) + track_event( + 'Add Email Requested', + success: success, + errors: errors, + **extra, + ) + end + + # Tracks When users visit the add phone page + def add_phone_setup_visit + track_event( + 'Phone Setup Visited', + ) + end + # When a user views the "you are already signed in with the following email" screen def authentication_confirmation track_event('Authentication Confirmation') @@ -220,6 +254,27 @@ def broken_personal_key_regenerated track_event('Broken Personal Key: Regenerated') end + # Tracks users going back or cancelling acoount recovery + def cancel_account_reset_recovery + track_event('Account Reset: Cancel Account Recovery Options') + end + + # @param [String] redirect_url URL user was directed to + # @param [String, nil] step which step + # @param [String, nil] location which part of a step, if applicable + # @param ["idv", String, nil] flow which flow + # User was redirected to the login.gov contact page + def contact_redirect(redirect_url:, step: nil, location: nil, flow: nil, **extra) + track_event( + 'Contact Page Redirect', + redirect_url: redirect_url, + step: step, + location: location, + flow: flow, + **extra, + ) + end + # @param [String, nil] error error message # @param [String, nil] uuid document capture session uuid # @param [String, nil] result_id document capture session result id @@ -280,39 +335,29 @@ def email_deletion_request(success:, errors:, **extra) # @param [Boolean] success # @param [Hash] errors - # Tracks request for adding new emails to an account - def add_email_request(success:, errors:, **extra) + # Tracks if Email Language is updated + def email_language_updated(success:, errors:, **extra) track_event( - 'Add Email Requested', + 'Email Language: Updated', success: success, errors: errors, **extra, ) end - # @param [Boolean] success - # Tracks request for resending confirmation for new emails to an account - def resend_add_email_request(success:, **extra) - track_event( - 'Resend Add Email Requested', - success: success, - **extra, - ) - end - # Tracks if Email Language is visited def email_language_visited track_event('Email Language: Visited') end - # @param [Boolean] success - # @param [Hash] errors - # Tracks if Email Language is updated - def email_language_updated(success:, errors:, **extra) + # Logs after an email is sent + # @param [String] action type of email being sent + # @param [String, nil] ses_message_id AWS SES Message ID + def email_sent(action:, ses_message_id:, **extra) track_event( - 'Email Language: Updated', - success: success, - errors: errors, + 'Email Sent', + action: action, + ses_message_id: ses_message_id, **extra, ) end @@ -489,55 +534,31 @@ def idv_address_visit track_event('IdV: address visited') end - # @param [String] step the step that the user was on when they clicked cancel - # @param [String] request_came_from the controller and action from the - # source such as "users/sessions#new" - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The user clicked cancel during IDV (presented with an option to go back or confirm) - def idv_cancellation_visited( - step:, - request_came_from:, - proofing_components: nil, - **extra - ) - track_event( - 'IdV: cancellation visited', - step: step, - request_came_from: request_came_from, - proofing_components: proofing_components, - **extra, - ) - end - - # @param [Integer] failed_capture_attempts Number of failed Acuant SDK attempts - # @param [Integer] failed_submission_attempts Number of failed Acuant doc submissions - # @param [String] field Image form field - # @param [String] flow_path Document capture path ("hybrid" or "standard") - # The number of acceptable failed attempts (maxFailedAttemptsBeforeNativeCamera) has been met - # or exceeded, and the system has forced the use of the native camera, rather than Acuant's - # camera, on mobile devices. - def idv_native_camera_forced( - failed_capture_attempts:, - failed_submission_attempts:, - field:, - flow_path:, + # Tracks if request to get address candidates from ArcGIS fails + # @param [String] exception_class + # @param [String] exception_message + # @param [Boolean] response_body_present + # @param [Hash] response_body + # @param [Integer] response_status_code + def idv_arcgis_request_failure( + exception_class:, + exception_message:, + response_body_present:, + response_body:, + response_status_code:, **extra ) track_event( - 'IdV: Native camera forced after failed attempts', - failed_capture_attempts: failed_capture_attempts, - failed_submission_attempts: failed_submission_attempts, - field: field, - flow_path: flow_path, + 'Request ArcGIS Address Candidates: request failed', + exception_class: exception_class, + exception_message: exception_message, + response_body_present: response_body_present, + response_body: response_body, + response_status_code: response_status_code, **extra, ) end - # The user visited the gpo confirm cancellation screen - def idv_gpo_confirm_start_over_visited - track_event('IdV: gpo confirm start over visited') - end - # @param [String] step the step that the user was on when they clicked cancel # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components # The user confirmed their choice to cancel going through IDV @@ -562,3358 +583,3345 @@ def idv_cancellation_go_back(step:, proofing_components: nil, **extra) ) end - # The user checked or unchecked the "By checking this box..." checkbox on the idv agreement step. - # (This is a frontend event.) - # @param [Boolean] checked Whether the user checked the checkbox - def idv_consent_checkbox_toggled(checked:, **extra) - track_event( - 'IdV: consent checkbox toggled', - checked: checked, - **extra, - ) - end - - # The user visited the "come back later" page shown during the GPO mailing flow + # @param [String] step the step that the user was on when they clicked cancel + # @param [String] request_came_from the controller and action from the + # source such as "users/sessions#new" # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - def idv_come_back_later_visit(proofing_components: nil, **extra) - track_event( - 'IdV: come back later visited', - proofing_components: proofing_components, - **extra, - ) - end - - # @param [String] flow_path Document capture path ("hybrid" or "standard") - # @param [String] in_person_cta_variant Variant testing bucket label - # The user clicked the troubleshooting option to start in-person proofing - def idv_verify_in_person_troubleshooting_option_clicked(flow_path:, in_person_cta_variant:, - **extra) + # The user clicked cancel during IDV (presented with an option to go back or confirm) + def idv_cancellation_visited( + step:, + request_came_from:, + proofing_components: nil, + **extra + ) track_event( - 'IdV: verify in person troubleshooting option clicked', - flow_path: flow_path, - in_person_cta_variant: in_person_cta_variant, + 'IdV: cancellation visited', + step: step, + request_came_from: request_came_from, + proofing_components: proofing_components, **extra, ) end - # @param [String] flow_path Document capture path ("hybrid" or "standard") - # @param [String] in_person_cta_variant Variant testing bucket label - # The user visited the in person proofing location step - def idv_in_person_location_visited(flow_path:, in_person_cta_variant:, **extra) + # The user visited the "come back later" page shown during the GPO mailing flow + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + def idv_come_back_later_visit(proofing_components: nil, **extra) track_event( - 'IdV: in person proofing location visited', - flow_path: flow_path, - in_person_cta_variant: in_person_cta_variant, + 'IdV: come back later visited', + proofing_components: proofing_components, **extra, ) end - # @param [Boolean] success - # @param [Integer] result_total - # @param [String] errors - # @param [String] exception_class - # @param [String] exception_message - # @param [Integer] response_status_code - # User submitted a search on the location search page and response received - def idv_in_person_locations_searched( - success:, - result_total: 0, - errors: nil, - exception_class: nil, - exception_message: nil, - response_status_code: nil, - **extra - ) + # The user checked or unchecked the "By checking this box..." checkbox on the idv agreement step. + # (This is a frontend event.) + # @param [Boolean] checked Whether the user checked the checkbox + def idv_consent_checkbox_toggled(checked:, **extra) track_event( - 'IdV: in person proofing location search submitted', - success: success, - result_total: result_total, - errors: errors, - exception_class: exception_class, - exception_message: exception_message, - response_status_code: response_status_code, + 'IdV: consent checkbox toggled', + checked: checked, **extra, ) end - # @param [String] selected_location Selected in-person location - # @param [String] in_person_cta_variant Variant testing bucket label - # @param [String] flow_path Document capture path ("hybrid" or "standard") - # The user submitted the in person proofing location step - def idv_in_person_location_submitted(selected_location:, in_person_cta_variant:, flow_path:, - **extra) - track_event( - 'IdV: in person proofing location submitted', - selected_location: selected_location, - flow_path: flow_path, - in_person_cta_variant: in_person_cta_variant, - **extra, - ) + # User has consented to share information with document upload and may + # view the "hybrid handoff" step next unless "skip_upload" param is true + def idv_doc_auth_agreement_submitted(**extra) + track_event('IdV: doc auth agreement submitted', **extra) end - # @param [String] flow_path Document capture path ("hybrid" or "standard") - # The user visited the in person proofing prepare step - def idv_in_person_prepare_visited(flow_path:, **extra) - track_event('IdV: in person proofing prepare visited', flow_path: flow_path, **extra) + def idv_doc_auth_agreement_visited(**extra) + track_event('IdV: doc auth agreement visited', **extra) end - # @param [String] flow_path Document capture path ("hybrid" or "standard") - # @param [String] in_person_cta_variant Variant testing bucket label - # The user submitted the in person proofing prepare step - def idv_in_person_prepare_submitted(flow_path:, in_person_cta_variant:, **extra) + def idv_doc_auth_cancel_link_sent_submitted(**extra) + track_event('IdV: doc auth cancel_link_sent submitted', **extra) + end + + # @identity.idp.previous_event_name IdV: in person proofing cancel_update_ssn submitted + def idv_doc_auth_cancel_update_ssn_submitted(**extra) + track_event('IdV: doc auth cancel_update_ssn submitted', **extra) + end + + def idv_doc_auth_capture_complete_visited(**extra) + track_event('IdV: doc auth capture_complete visited', **extra) + end + + def idv_doc_auth_document_capture_submitted(**extra) + track_event('IdV: doc auth document_capture submitted', **extra) + end + + def idv_doc_auth_document_capture_visited(**extra) + track_event('IdV: doc auth document_capture visited', **extra) + end + + # @param [String] step_name which step the user was on + # @param [Integer] remaining_attempts how many attempts the user has left before we throttle them + # The user visited an error page due to an encountering an exception talking to a proofing vendor + def idv_doc_auth_exception_visited(step_name:, remaining_attempts:, **extra) track_event( - 'IdV: in person proofing prepare submitted', - flow_path: flow_path, - in_person_cta_variant: in_person_cta_variant, + 'IdV: doc auth exception visited', + step_name: step_name, + remaining_attempts: remaining_attempts, **extra, ) end - # @param [String] nontransliterable_characters - # Nontransliterable characters submitted by user - def idv_in_person_proofing_nontransliterable_characters_submitted( - nontransliterable_characters:, - **extra - ) + # @identity.idp.previous_event_name IdV: doc auth send_link submitted + def idv_doc_auth_link_sent_submitted(**extra) + track_event('IdV: doc auth link_sent submitted', **extra) + end + + def idv_doc_auth_link_sent_visited(**extra) + track_event('IdV: doc auth link_sent visited', **extra) + end + + # @identity.idp.previous_event_name IdV: in person proofing optional verify_wait submitted + def idv_doc_auth_optional_verify_wait_submitted(**extra) + track_event('IdV: doc auth optional verify_wait submitted', **extra) + end + + def idv_doc_auth_randomizer_defaulted track_event( - 'IdV: in person proofing characters submitted could not be transliterated', - nontransliterable_characters: nontransliterable_characters, - **extra, + 'IdV: doc_auth random vendor error', + error: 'document_capture_session_uuid_key missing', ) end - def idv_in_person_proofing_residential_address_submitted(**extra) - track_event('IdV: in person proofing residential address submitted', **extra) + # @identity.idp.previous_event_name IdV: in person proofing redo_address submitted + def idv_doc_auth_redo_address_submitted(**extra) + track_event('IdV: doc auth redo_address submitted', **extra) + end + + def idv_doc_auth_redo_document_capture_submitted(**extra) + track_event('IdV: doc auth redo_document_capture submitted', **extra) + end + + def idv_doc_auth_redo_ssn_submitted(**extra) + track_event('IdV: doc auth redo_ssn submitted', **extra) + end + + # @identity.idp.previous_event_name IdV: in person proofing ssn submitted + def idv_doc_auth_ssn_submitted(**extra) + track_event('IdV: doc auth ssn submitted', **extra) + end + + # @identity.idp.previous_event_name IdV: in person proofing ssn visited + def idv_doc_auth_ssn_visited(**extra) + track_event('IdV: doc auth ssn visited', **extra) end - # @param [String] flow_path - # @param [String] step - # @param [Integer] step_count - # @param [String] analytics_id - # @param [Boolean] irs_reproofing # @param [Boolean] success # @param [Hash] errors - # @param [Boolean] same_address_as_id - # address submitted by user - def idv_in_person_proofing_address_submitted( - flow_path: nil, - step: nil, - step_count: nil, - analytics_id: nil, - irs_reproofing: nil, - success: nil, - errors: nil, - same_address_as_id: nil, + # @param [Integer] attempts + # @param [Integer] remaining_attempts + # @param [String] user_id + # @param [String] flow_path + # The document capture image uploaded was locally validated during the IDV process + def idv_doc_auth_submitted_image_upload_form( + success:, + errors:, + remaining_attempts:, + flow_path:, + attempts: nil, + user_id: nil, **extra ) track_event( - 'IdV: in person proofing address submitted', - flow_path: flow_path, - step: step, - step_count: step_count, - analytics_id: analytics_id, - irs_reproofing: irs_reproofing, + 'IdV: doc auth image upload form submitted', success: success, errors: errors, - same_address_as_id: same_address_as_id, + attempts: attempts, + remaining_attempts: remaining_attempts, + user_id: user_id, + flow_path: flow_path, **extra, ) end + # @param [Boolean] success + # @param [Hash] errors + # @param [String] exception + # @param [Boolean] billed + # @param [String] doc_auth_result + # @param [String] state + # @param [String] state_id_type + # @param [Boolean] async + # @param [Integer] attempts + # @param [Integer] remaining_attempts + # @param [Hash] client_image_metrics # @param [String] flow_path - # @param [String] step - # @param [Integer] step_count - # @param [String] analytics_id - # @param [Boolean] irs_reproofing - # address page visited - def idv_in_person_proofing_address_visited( - flow_path: nil, - step: nil, - step_count: nil, - analytics_id: nil, - irs_reproofing: nil, + # The document capture image was uploaded to vendor during the IDV process + def idv_doc_auth_submitted_image_upload_vendor( + success:, + errors:, + exception:, + state:, + state_id_type:, + async:, attempts:, + remaining_attempts:, + client_image_metrics:, + flow_path:, + billed: nil, + doc_auth_result: nil, **extra ) track_event( - 'IdV: in person proofing address visited', - flow_path: flow_path, - step: step, - step_count: step_count, - analytics_id: analytics_id, - irs_reproofing: irs_reproofing, - **extra, - ) - end - - # @param [String] flow_path - # @param [String] step - # @param [Integer] step_count - # @param [String] analytics_id - # @param [Boolean] irs_reproofing - # @param [Boolean] success - # @param [Hash] errors - # @param [Boolean, nil] same_address_as_id - # User clicked cancel on update address page - def idv_in_person_proofing_cancel_update_address( - flow_path: nil, - step: nil, - step_count: nil, - analytics_id: nil, - irs_reproofing: nil, - success: nil, - errors: nil, - same_address_as_id: nil, - **extra - ) - track_event( - 'IdV: in person proofing cancel_update_address submitted', - flow_path: flow_path, - step: step, - step_count: step_count, - analytics_id: analytics_id, - irs_reproofing: irs_reproofing, + 'IdV: doc auth image upload vendor submitted', success: success, errors: errors, - same_address_as_id: same_address_as_id, + exception: exception, + billed: billed, + doc_auth_result: doc_auth_result, + state: state, + state_id_type: state_id_type, + async: async, + attempts: attempts, + remaining_attempts: remaining_attempts, + client_image_metrics: client_image_metrics, + flow_path: flow_path, **extra, ) end - # @param [String] flow_path - # @param [String] step - # @param [Integer] step_count - # @param [String] analytics_id - # @param [Boolean] irs_reproofing # @param [Boolean] success # @param [Hash] errors - # @param [Boolean] same_address_as_id - # User clicked cancel on update state id page - def idv_in_person_proofing_cancel_update_state_id( - flow_path: nil, - step: nil, - step_count: nil, - analytics_id: nil, - irs_reproofing: nil, - success: nil, - errors: nil, - same_address_as_id: nil, + # @param [String] user_id + # @param [Integer] remaining_attempts + # @param [Hash] pii_like_keypaths + # @param [String] flow_path + # The PII that came back from the document capture vendor was validated + def idv_doc_auth_submitted_pii_validation( + success:, + errors:, + remaining_attempts:, + pii_like_keypaths:, + flow_path:, + user_id: nil, **extra ) track_event( - 'IdV: in person proofing cancel_update_state_id submitted', - flow_path: flow_path, - step: step, - step_count: step_count, - analytics_id: analytics_id, - irs_reproofing: irs_reproofing, + 'IdV: doc auth image upload vendor pii validation', success: success, errors: errors, - same_address_as_id: same_address_as_id, + user_id: user_id, + remaining_attempts: remaining_attempts, + pii_like_keypaths: pii_like_keypaths, + flow_path: flow_path, **extra, ) end - # @param [String] flow_path - # @param [String] step - # @param [Integer] step_count - # @param [String] analytics_id - # @param [Boolean] irs_reproofing - # @param [Boolean] success - # @param [Hash] errors - # @param [Boolean] same_address_as_id - # User submitted state id on redo state id page - def idv_in_person_proofing_redo_state_id_submitted( - flow_path: nil, - step: nil, - step_count: nil, - analytics_id: nil, - irs_reproofing: nil, - success: nil, - errors: nil, - same_address_as_id: nil, - **extra - ) - track_event( - 'IdV: in person proofing redo_state_id submitted', - flow_path: flow_path, - step: step, - step_count: step_count, - analytics_id: analytics_id, - irs_reproofing: irs_reproofing, - success: success, - errors: errors, - same_address_as_id: same_address_as_id, - **extra, - ) + # The "hybrid handoff" step: Desktop user has submitted their choice to + # either continue via desktop ("document_capture" destination) or switch + # to mobile phone ("send_link" destination) to perform document upload. + # Mobile users still log this event but with skip_upload_step = true + def idv_doc_auth_upload_submitted(**extra) + track_event('IdV: doc auth upload submitted', **extra) end - # @param [String] flow_path - # @param [String] step - # @param [Integer] step_count - # @param [String] analytics_id - # @param [Boolean] irs_reproofing - # @param [Boolean] success - # @param [Hash] errors - # @param [Boolean, nil] same_address_as_id - # User submitted state id - def idv_in_person_proofing_state_id_submitted( - flow_path: nil, - step: nil, - step_count: nil, - analytics_id: nil, - irs_reproofing: nil, - success: nil, - errors: nil, - same_address_as_id: nil, - **extra - ) - track_event( - 'IdV: in person proofing state_id submitted', - flow_path: flow_path, - step: step, - step_count: step_count, - analytics_id: analytics_id, - irs_reproofing: irs_reproofing, - success: success, - errors: errors, - same_address_as_id: same_address_as_id, - **extra, - ) + # Desktop user has reached the above "hybrid handoff" view + def idv_doc_auth_upload_visited(**extra) + track_event('IdV: doc auth upload visited', **extra) end - # @param [String] flow_path - # @param [String] step - # @param [Integer] step_count - # @param [String] analytics_id - # @param [Boolean] irs_reproofing - # State id page visited - def idv_in_person_proofing_state_id_visited( - flow_path: nil, - step: nil, - step_count: nil, - analytics_id: nil, - irs_reproofing: nil, + # @identity.idp.previous_event_name IdV: doc auth optional verify_wait submitted + def idv_doc_auth_verify_proofing_results(**extra) + track_event('IdV: doc auth verify proofing results', **extra) + end + + # @identity.idp.previous_event_name IdV: in person proofing verify submitted + def idv_doc_auth_verify_submitted(**extra) + track_event('IdV: doc auth verify submitted', **extra) + end + + # @identity.idp.previous_event_name IdV: in person proofing verify visited + def idv_doc_auth_verify_visited(**extra) + track_event('IdV: doc auth verify visited', **extra) + end + + # @identity.idp.previous_event_name IdV: in person proofing verify_wait visited + def idv_doc_auth_verify_wait_step_visited(**extra) + track_event('IdV: doc auth verify_wait visited', **extra) + end + + # @param [String] step_name + # @param [Integer] remaining_attempts + # The user was sent to a warning page during the IDV flow + def idv_doc_auth_warning_visited( + step_name:, + remaining_attempts:, **extra ) track_event( - 'IdV: in person proofing state_id visited', - flow_path: flow_path, - step: step, - step_count: step_count, - analytics_id: analytics_id, - irs_reproofing: irs_reproofing, + 'IdV: doc auth warning visited', + step_name: step_name, + remaining_attempts: remaining_attempts, **extra, ) end - # @param [String] flow_path Document capture path ("hybrid" or "standard") - # The user visited the in person proofing switch_back step - def idv_in_person_switch_back_visited(flow_path:, **extra) - track_event('IdV: in person proofing switch_back visited', flow_path: flow_path, **extra) + def idv_doc_auth_welcome_submitted(**extra) + track_event('IdV: doc auth welcome submitted', **extra) end - # @param [String] flow_path Document capture path ("hybrid" or "standard") - # The user submitted the in person proofing switch_back step - def idv_in_person_switch_back_submitted(flow_path:, **extra) - track_event('IdV: in person proofing switch_back submitted', flow_path: flow_path, **extra) + def idv_doc_auth_welcome_visited(**extra) + track_event('IdV: doc auth welcome visited', **extra) end - # @param [String] in_person_cta_variant Variant testing bucket label + # @param [Boolean] success + # @param [String, nil] deactivation_reason Reason user's profile was deactivated, if any. + # @param [Boolean] fraud_review_pending Profile is under review for fraud + # @param [Boolean] fraud_rejection Profile is rejected due to fraud + # @param [Boolean] gpo_verification_pending Profile is awaiting gpo verificaiton # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The user visited the "ready to verify" page for the in person proofing flow - def idv_in_person_ready_to_verify_visit(in_person_cta_variant: nil, proofing_components: nil, - **extra) + # Tracks the last step of IDV, indicates the user successfully proofed + def idv_final( + success:, + fraud_review_pending:, + fraud_rejection:, + gpo_verification_pending:, + deactivation_reason: nil, + proofing_components: nil, + **extra + ) track_event( - 'IdV: in person ready to verify visited', - in_person_cta_variant: in_person_cta_variant, + 'IdV: final resolution', + success: success, + fraud_review_pending: fraud_review_pending, + fraud_rejection: fraud_rejection, + gpo_verification_pending: gpo_verification_pending, + deactivation_reason: deactivation_reason, proofing_components: proofing_components, **extra, ) end - # The user clicked the sp link on the "ready to verify" page - def idv_in_person_ready_to_verify_sp_link_clicked(**extra) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # User visited forgot password page + def idv_forgot_password(proofing_components: nil, **extra) track_event( - 'IdV: user clicked sp link on ready to verify page', + 'IdV: forgot password visited', + proofing_components: proofing_components, **extra, ) end - # The user clicked the what to bring link on the "ready to verify" page - def idv_in_person_ready_to_verify_what_to_bring_link_clicked(**extra) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # User confirmed forgot password + def idv_forgot_password_confirmed(proofing_components: nil, **extra) track_event( - 'IdV: user clicked what to bring link on ready to verify page', + 'IdV: forgot password confirmed', + proofing_components: proofing_components, **extra, ) end - # A job to check USPS notifications about in-person enrollment status updates has started - def idv_in_person_proofing_enrollments_ready_for_status_check_job_started(**extra) - track_event( - 'InPersonEnrollmentsReadyForStatusCheckJob: Job started', + # @param [DateTime] enqueued_at + # @param [Boolean] resend + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # GPO letter was enqueued and the time at which it was enqueued + def idv_gpo_address_letter_enqueued(enqueued_at:, resend:, proofing_components: nil, **extra) + track_event( + 'IdV: USPS address letter enqueued', + enqueued_at: enqueued_at, + resend: resend, + proofing_components: proofing_components, **extra, ) end - # A job to check USPS notifications about in-person enrollment status updates has completed - # @param [Integer] fetched_items items fetched - # @param [Integer] processed_items items fetched and processed - # @param [Integer] deleted_items items fetched, processed, and then deleted from the queue - # @param [Integer] valid_items items that could be successfully used to update a record - # @param [Integer] invalid_items items that couldn't be used to update a record - # @param [Integer] incomplete_items fetched items not processed nor deleted from the queue - # @param [Integer] deletion_failed_items processed items that we failed to delete - def idv_in_person_proofing_enrollments_ready_for_status_check_job_completed( - fetched_items:, - processed_items:, - deleted_items:, - valid_items:, - invalid_items:, - incomplete_items:, - deletion_failed_items:, - **extra - ) + # @param [Boolean] resend + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # GPO letter was requested + def idv_gpo_address_letter_requested(resend:, proofing_components: nil, **extra) track_event( - 'InPersonEnrollmentsReadyForStatusCheckJob: Job completed', - fetched_items:, - processed_items:, - deleted_items:, - valid_items:, - invalid_items:, - incomplete_items:, - deletion_failed_items:, + 'IdV: USPS address letter requested', + resend: resend, + proofing_components: proofing_components, **extra, ) end - # A job to check USPS notifications about in-person enrollment status updates - # has encountered an error - # @param [String] exception_class - # @param [String] exception_message - def idv_in_person_proofing_enrollments_ready_for_status_check_job_ingestion_error( - exception_class:, - exception_message:, + # @param [Boolean] letter_already_sent + # GPO address visited + def idv_gpo_address_visited( + letter_already_sent:, **extra ) track_event( - 'InPersonEnrollmentsReadyForStatusCheckJob: Ingestion error', - exception_class:, - exception_message:, + 'IdV: USPS address visited', + letter_already_sent: letter_already_sent, **extra, ) end - # User has consented to share information with document upload and may - # view the "hybrid handoff" step next unless "skip_upload" param is true - def idv_doc_auth_agreement_submitted(**extra) - track_event('IdV: doc auth agreement submitted', **extra) - end - - def idv_doc_auth_agreement_visited(**extra) - track_event('IdV: doc auth agreement visited', **extra) - end - - def idv_doc_auth_cancel_link_sent_submitted(**extra) - track_event('IdV: doc auth cancel_link_sent submitted', **extra) - end - - # @identity.idp.previous_event_name IdV: in person proofing cancel_update_ssn submitted - def idv_doc_auth_cancel_update_ssn_submitted(**extra) - track_event('IdV: doc auth cancel_update_ssn submitted', **extra) - end - - def idv_doc_auth_capture_complete_visited(**extra) - track_event('IdV: doc auth capture_complete visited', **extra) - end - - def idv_doc_auth_document_capture_visited(**extra) - track_event('IdV: doc auth document_capture visited', **extra) - end - - def idv_doc_auth_document_capture_submitted(**extra) - track_event('IdV: doc auth document_capture submitted', **extra) + # The user visited the gpo confirm cancellation screen + def idv_gpo_confirm_start_over_visited + track_event('IdV: gpo confirm start over visited') end - # @param [String] step_name which step the user was on - # @param [Integer] remaining_attempts how many attempts the user has left before we throttle them - # The user visited an error page due to an encountering an exception talking to a proofing vendor - def idv_doc_auth_exception_visited(step_name:, remaining_attempts:, **extra) + # @identity.idp.previous_event_name Account verification submitted + # @param [Boolean] success + # @param [Hash] errors + # @param [Hash] pii_like_keypaths + # GPO verification submitted + def idv_gpo_verification_submitted( + success:, + errors:, + pii_like_keypaths:, + **extra + ) track_event( - 'IdV: doc auth exception visited', - step_name: step_name, - remaining_attempts: remaining_attempts, + 'IdV: GPO verification submitted', + success: success, + errors: errors, + pii_like_keypaths: pii_like_keypaths, **extra, ) end - # @identity.idp.previous_event_name IdV: doc auth send_link submitted - def idv_doc_auth_link_sent_submitted(**extra) - track_event('IdV: doc auth link_sent submitted', **extra) - end - - def idv_doc_auth_link_sent_visited(**extra) - track_event('IdV: doc auth link_sent visited', **extra) - end - - # @identity.idp.previous_event_name IdV: in person proofing optional verify_wait submitted - def idv_doc_auth_optional_verify_wait_submitted(**extra) - track_event('IdV: doc auth optional verify_wait submitted', **extra) + # @identity.idp.previous_event_name Account verification visited + # GPO verification visited + def idv_gpo_verification_visited + track_event('IdV: GPO verification visited') end - def idv_doc_auth_redo_document_capture_submitted(**extra) - track_event('IdV: doc auth redo_document_capture submitted', **extra) + # Tracks emails that are initiated during InPerson::EmailReminderJob + # @param [String] email_type early or late + # @param [String] enrollment_id + def idv_in_person_email_reminder_job_email_initiated( + email_type:, + enrollment_id:, + **extra + ) + track_event( + 'InPerson::EmailReminderJob: Reminder email initiated', + email_type: email_type, + enrollment_id: enrollment_id, + **extra, + ) end - # @identity.idp.previous_event_name IdV: in person proofing ssn submitted - def idv_doc_auth_ssn_submitted(**extra) - track_event('IdV: doc auth ssn submitted', **extra) + # Tracks exceptions that are raised when running InPerson::EmailReminderJob + # @param [String] enrollment_id + # @param [String] exception_class + # @param [String] exception_message + def idv_in_person_email_reminder_job_exception( + enrollment_id:, + exception_class: nil, + exception_message: nil, + **extra + ) + track_event( + 'InPerson::EmailReminderJob: Exception raised when attempting to send reminder email', + enrollment_id: enrollment_id, + exception_class: exception_class, + exception_message: exception_message, + **extra, + ) end - # @identity.idp.previous_event_name IdV: in person proofing ssn visited - def idv_doc_auth_ssn_visited(**extra) - track_event('IdV: doc auth ssn visited', **extra) + # @param [String] selected_location Selected in-person location + # @param [String] in_person_cta_variant Variant testing bucket label + # @param [String] flow_path Document capture path ("hybrid" or "standard") + # The user submitted the in person proofing location step + def idv_in_person_location_submitted(selected_location:, in_person_cta_variant:, flow_path:, + **extra) + track_event( + 'IdV: in person proofing location submitted', + selected_location: selected_location, + flow_path: flow_path, + in_person_cta_variant: in_person_cta_variant, + **extra, + ) end - # @identity.idp.previous_event_name IdV: in person proofing redo_address submitted - def idv_doc_auth_redo_address_submitted(**extra) - track_event('IdV: doc auth redo_address submitted', **extra) + # @param [String] flow_path Document capture path ("hybrid" or "standard") + # @param [String] in_person_cta_variant Variant testing bucket label + # The user visited the in person proofing location step + def idv_in_person_location_visited(flow_path:, in_person_cta_variant:, **extra) + track_event( + 'IdV: in person proofing location visited', + flow_path: flow_path, + in_person_cta_variant: in_person_cta_variant, + **extra, + ) end - def idv_doc_auth_redo_ssn_submitted(**extra) - track_event('IdV: doc auth redo_ssn submitted', **extra) + # Tracks if request to get USPS in-person proofing locations fails + # @param [String] exception_class + # @param [String] exception_message + # @param [Boolean] response_body_present + # @param [Hash] response_body + # @param [Integer] response_status_code + def idv_in_person_locations_request_failure( + exception_class:, + exception_message:, + response_body_present:, + response_body:, + response_status_code:, + **extra + ) + track_event( + 'Request USPS IPP locations: request failed', + exception_class: exception_class, + exception_message: exception_message, + response_body_present: response_body_present, + response_body: response_body, + response_status_code: response_status_code, + **extra, + ) end # @param [Boolean] success - # @param [Hash] errors - # @param [Integer] attempts - # @param [Integer] remaining_attempts - # @param [String] user_id - # @param [String] flow_path - # The document capture image uploaded was locally validated during the IDV process - def idv_doc_auth_submitted_image_upload_form( + # @param [Integer] result_total + # @param [String] errors + # @param [String] exception_class + # @param [String] exception_message + # @param [Integer] response_status_code + # User submitted a search on the location search page and response received + def idv_in_person_locations_searched( success:, - errors:, - remaining_attempts:, - flow_path:, - attempts: nil, - user_id: nil, + result_total: 0, + errors: nil, + exception_class: nil, + exception_message: nil, + response_status_code: nil, **extra ) track_event( - 'IdV: doc auth image upload form submitted', + 'IdV: in person proofing location search submitted', success: success, + result_total: result_total, errors: errors, - attempts: attempts, - remaining_attempts: remaining_attempts, - user_id: user_id, - flow_path: flow_path, + exception_class: exception_class, + exception_message: exception_message, + response_status_code: response_status_code, **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # @param [String] exception - # @param [Boolean] billed - # @param [String] doc_auth_result - # @param [String] state - # @param [String] state_id_type - # @param [Boolean] async - # @param [Integer] attempts - # @param [Integer] remaining_attempts - # @param [Hash] client_image_metrics - # @param [String] flow_path - # The document capture image was uploaded to vendor during the IDV process - def idv_doc_auth_submitted_image_upload_vendor( - success:, - errors:, - exception:, - state:, - state_id_type:, - async:, attempts:, - remaining_attempts:, - client_image_metrics:, - flow_path:, - billed: nil, - doc_auth_result: nil, - **extra - ) + # @param [String] flow_path Document capture path ("hybrid" or "standard") + # @param [String] in_person_cta_variant Variant testing bucket label + # The user submitted the in person proofing prepare step + def idv_in_person_prepare_submitted(flow_path:, in_person_cta_variant:, **extra) track_event( - 'IdV: doc auth image upload vendor submitted', - success: success, - errors: errors, - exception: exception, - billed: billed, - doc_auth_result: doc_auth_result, - state: state, - state_id_type: state_id_type, - async: async, - attempts: attempts, - remaining_attempts: remaining_attempts, - client_image_metrics: client_image_metrics, + 'IdV: in person proofing prepare submitted', flow_path: flow_path, + in_person_cta_variant: in_person_cta_variant, **extra, ) end - def idv_doc_auth_randomizer_defaulted - track_event( - 'IdV: doc_auth random vendor error', - error: 'document_capture_session_uuid_key missing', - ) + # @param [String] flow_path Document capture path ("hybrid" or "standard") + # The user visited the in person proofing prepare step + def idv_in_person_prepare_visited(flow_path:, **extra) + track_event('IdV: in person proofing prepare visited', flow_path: flow_path, **extra) end + # @param [String] flow_path + # @param [String] step + # @param [Integer] step_count + # @param [String] analytics_id + # @param [Boolean] irs_reproofing # @param [Boolean] success # @param [Hash] errors - # @param [String] user_id - # @param [Integer] remaining_attempts - # @param [Hash] pii_like_keypaths - # @param [String] flow_path - # The PII that came back from the document capture vendor was validated - def idv_doc_auth_submitted_pii_validation( - success:, - errors:, - remaining_attempts:, - pii_like_keypaths:, - flow_path:, - user_id: nil, + # @param [Boolean] same_address_as_id + # address submitted by user + def idv_in_person_proofing_address_submitted( + flow_path: nil, + step: nil, + step_count: nil, + analytics_id: nil, + irs_reproofing: nil, + success: nil, + errors: nil, + same_address_as_id: nil, **extra ) track_event( - 'IdV: doc auth image upload vendor pii validation', + 'IdV: in person proofing address submitted', + flow_path: flow_path, + step: step, + step_count: step_count, + analytics_id: analytics_id, + irs_reproofing: irs_reproofing, success: success, errors: errors, - user_id: user_id, - remaining_attempts: remaining_attempts, - pii_like_keypaths: pii_like_keypaths, - flow_path: flow_path, + same_address_as_id: same_address_as_id, **extra, ) end - # The "hybrid handoff" step: Desktop user has submitted their choice to - # either continue via desktop ("document_capture" destination) or switch - # to mobile phone ("send_link" destination) to perform document upload. - # Mobile users still log this event but with skip_upload_step = true - def idv_doc_auth_upload_submitted(**extra) - track_event('IdV: doc auth upload submitted', **extra) - end - - # Desktop user has reached the above "hybrid handoff" view - def idv_doc_auth_upload_visited(**extra) - track_event('IdV: doc auth upload visited', **extra) - end - - # @identity.idp.previous_event_name IdV: in person proofing verify submitted - def idv_doc_auth_verify_submitted(**extra) - track_event('IdV: doc auth verify submitted', **extra) - end - - # @identity.idp.previous_event_name IdV: in person proofing verify visited - def idv_doc_auth_verify_visited(**extra) - track_event('IdV: doc auth verify visited', **extra) - end - - # @identity.idp.previous_event_name IdV: doc auth optional verify_wait submitted - def idv_doc_auth_verify_proofing_results(**extra) - track_event('IdV: doc auth verify proofing results', **extra) - end - - # @identity.idp.previous_event_name IdV: in person proofing verify_wait visited - def idv_doc_auth_verify_wait_step_visited(**extra) - track_event('IdV: doc auth verify_wait visited', **extra) - end - - # @param [String] step_name - # @param [Integer] remaining_attempts - # The user was sent to a warning page during the IDV flow - def idv_doc_auth_warning_visited( - step_name:, - remaining_attempts:, + # @param [String] flow_path + # @param [String] step + # @param [Integer] step_count + # @param [String] analytics_id + # @param [Boolean] irs_reproofing + # address page visited + def idv_in_person_proofing_address_visited( + flow_path: nil, + step: nil, + step_count: nil, + analytics_id: nil, + irs_reproofing: nil, **extra ) track_event( - 'IdV: doc auth warning visited', - step_name: step_name, - remaining_attempts: remaining_attempts, + 'IdV: in person proofing address visited', + flow_path: flow_path, + step: step, + step_count: step_count, + analytics_id: analytics_id, + irs_reproofing: irs_reproofing, **extra, ) end - def idv_doc_auth_welcome_submitted(**extra) - track_event('IdV: doc auth welcome submitted', **extra) - end - - def idv_doc_auth_welcome_visited(**extra) - track_event('IdV: doc auth welcome visited', **extra) + # @param [String] flow_path + # @param [String] step + # @param [Integer] step_count + # @param [String] analytics_id + # @param [Boolean] irs_reproofing + # @param [Boolean] success + # @param [Hash] errors + # @param [Boolean, nil] same_address_as_id + # User clicked cancel on update address page + def idv_in_person_proofing_cancel_update_address( + flow_path: nil, + step: nil, + step_count: nil, + analytics_id: nil, + irs_reproofing: nil, + success: nil, + errors: nil, + same_address_as_id: nil, + **extra + ) + track_event( + 'IdV: in person proofing cancel_update_address submitted', + flow_path: flow_path, + step: step, + step_count: step_count, + analytics_id: analytics_id, + irs_reproofing: irs_reproofing, + success: success, + errors: errors, + same_address_as_id: same_address_as_id, + **extra, + ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # User visited forgot password page - def idv_forgot_password(proofing_components: nil, **extra) + # @param [String] flow_path + # @param [String] step + # @param [Integer] step_count + # @param [String] analytics_id + # @param [Boolean] irs_reproofing + # @param [Boolean] success + # @param [Hash] errors + # @param [Boolean] same_address_as_id + # User clicked cancel on update state id page + def idv_in_person_proofing_cancel_update_state_id( + flow_path: nil, + step: nil, + step_count: nil, + analytics_id: nil, + irs_reproofing: nil, + success: nil, + errors: nil, + same_address_as_id: nil, + **extra + ) track_event( - 'IdV: forgot password visited', - proofing_components: proofing_components, + 'IdV: in person proofing cancel_update_state_id submitted', + flow_path: flow_path, + step: step, + step_count: step_count, + analytics_id: analytics_id, + irs_reproofing: irs_reproofing, + success: success, + errors: errors, + same_address_as_id: same_address_as_id, **extra, ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # User confirmed forgot password - def idv_forgot_password_confirmed(proofing_components: nil, **extra) + # A job to check USPS notifications about in-person enrollment status updates has completed + # @param [Integer] fetched_items items fetched + # @param [Integer] processed_items items fetched and processed + # @param [Integer] deleted_items items fetched, processed, and then deleted from the queue + # @param [Integer] valid_items items that could be successfully used to update a record + # @param [Integer] invalid_items items that couldn't be used to update a record + # @param [Integer] incomplete_items fetched items not processed nor deleted from the queue + # @param [Integer] deletion_failed_items processed items that we failed to delete + def idv_in_person_proofing_enrollments_ready_for_status_check_job_completed( + fetched_items:, + processed_items:, + deleted_items:, + valid_items:, + invalid_items:, + incomplete_items:, + deletion_failed_items:, + **extra + ) track_event( - 'IdV: forgot password confirmed', - proofing_components: proofing_components, + 'InPersonEnrollmentsReadyForStatusCheckJob: Job completed', + fetched_items:, + processed_items:, + deleted_items:, + valid_items:, + invalid_items:, + incomplete_items:, + deletion_failed_items:, **extra, ) end - # @param [DateTime] enqueued_at - # @param [Boolean] resend - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # GPO letter was enqueued and the time at which it was enqueued - def idv_gpo_address_letter_enqueued(enqueued_at:, resend:, proofing_components: nil, **extra) + # A job to check USPS notifications about in-person enrollment status updates + # has encountered an error + # @param [String] exception_class + # @param [String] exception_message + def idv_in_person_proofing_enrollments_ready_for_status_check_job_ingestion_error( + exception_class:, + exception_message:, + **extra + ) track_event( - 'IdV: USPS address letter enqueued', - enqueued_at: enqueued_at, - resend: resend, - proofing_components: proofing_components, + 'InPersonEnrollmentsReadyForStatusCheckJob: Ingestion error', + exception_class:, + exception_message:, **extra, ) end - # @param [Boolean] resend - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # GPO letter was requested - def idv_gpo_address_letter_requested(resend:, proofing_components: nil, **extra) + # A job to check USPS notifications about in-person enrollment status updates has started + def idv_in_person_proofing_enrollments_ready_for_status_check_job_started(**extra) track_event( - 'IdV: USPS address letter requested', - resend: resend, - proofing_components: proofing_components, + 'InPersonEnrollmentsReadyForStatusCheckJob: Job started', **extra, ) end - # @param [Boolean] letter_already_sent - # GPO address visited - def idv_gpo_address_visited( - letter_already_sent:, + # @param [String] nontransliterable_characters + # Nontransliterable characters submitted by user + def idv_in_person_proofing_nontransliterable_characters_submitted( + nontransliterable_characters:, **extra ) track_event( - 'IdV: USPS address visited', - letter_already_sent: letter_already_sent, + 'IdV: in person proofing characters submitted could not be transliterated', + nontransliterable_characters: nontransliterable_characters, **extra, ) end - # @identity.idp.previous_event_name Account verification submitted + # @param [String] flow_path + # @param [String] step + # @param [Integer] step_count + # @param [String] analytics_id + # @param [Boolean] irs_reproofing # @param [Boolean] success # @param [Hash] errors - # @param [Hash] pii_like_keypaths - # GPO verification submitted - def idv_gpo_verification_submitted( - success:, - errors:, - pii_like_keypaths:, + # @param [Boolean] same_address_as_id + # User submitted state id on redo state id page + def idv_in_person_proofing_redo_state_id_submitted( + flow_path: nil, + step: nil, + step_count: nil, + analytics_id: nil, + irs_reproofing: nil, + success: nil, + errors: nil, + same_address_as_id: nil, **extra ) track_event( - 'IdV: GPO verification submitted', + 'IdV: in person proofing redo_state_id submitted', + flow_path: flow_path, + step: step, + step_count: step_count, + analytics_id: analytics_id, + irs_reproofing: irs_reproofing, success: success, errors: errors, - pii_like_keypaths: pii_like_keypaths, + same_address_as_id: same_address_as_id, **extra, ) end - # @identity.idp.previous_event_name Account verification visited - # GPO verification visited - def idv_gpo_verification_visited - track_event('IdV: GPO verification visited') - end - - # User visits IdV - def idv_intro_visit - track_event('IdV: intro visited') + def idv_in_person_proofing_residential_address_submitted(**extra) + track_event('IdV: in person proofing residential address submitted', **extra) end + # @param [String] flow_path + # @param [String] step + # @param [Integer] step_count + # @param [String] analytics_id + # @param [Boolean] irs_reproofing # @param [Boolean] success - # @param [String, nil] deactivation_reason Reason user's profile was deactivated, if any. - # @param [Boolean] fraud_review_pending Profile is under review for fraud - # @param [Boolean] fraud_rejection Profile is rejected due to fraud - # @param [Boolean] gpo_verification_pending Profile is awaiting gpo verificaiton - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # Tracks the last step of IDV, indicates the user successfully proofed - def idv_final( - success:, - fraud_review_pending:, - fraud_rejection:, - gpo_verification_pending:, - deactivation_reason: nil, - proofing_components: nil, + # @param [Hash] errors + # @param [Boolean, nil] same_address_as_id + # User submitted state id + def idv_in_person_proofing_state_id_submitted( + flow_path: nil, + step: nil, + step_count: nil, + analytics_id: nil, + irs_reproofing: nil, + success: nil, + errors: nil, + same_address_as_id: nil, **extra ) track_event( - 'IdV: final resolution', + 'IdV: in person proofing state_id submitted', + flow_path: flow_path, + step: step, + step_count: step_count, + analytics_id: analytics_id, + irs_reproofing: irs_reproofing, success: success, - fraud_review_pending: fraud_review_pending, - fraud_rejection: fraud_rejection, - gpo_verification_pending: gpo_verification_pending, - deactivation_reason: deactivation_reason, - proofing_components: proofing_components, - **extra, - ) - end - - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # User visited IDV personal key page - def idv_personal_key_visited(proofing_components: nil, **extra) - track_event( - 'IdV: personal key visited', - proofing_components: proofing_components, + errors: errors, + same_address_as_id: same_address_as_id, **extra, ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # @param [String, nil] deactivation_reason Reason profile was deactivated. - # @param [Boolean] fraud_review_pending Profile is under review for fraud - # @param [Boolean] fraud_rejection Profile is rejected due to fraud - # User submitted IDV personal key page - def idv_personal_key_submitted( - fraud_review_pending:, - fraud_rejection:, - proofing_components: nil, - deactivation_reason: nil, + # @param [String] flow_path + # @param [String] step + # @param [Integer] step_count + # @param [String] analytics_id + # @param [Boolean] irs_reproofing + # State id page visited + def idv_in_person_proofing_state_id_visited( + flow_path: nil, + step: nil, + step_count: nil, + analytics_id: nil, + irs_reproofing: nil, **extra ) track_event( - 'IdV: personal key submitted', - deactivation_reason: deactivation_reason, - fraud_review_pending: fraud_review_pending, - fraud_rejection: fraud_rejection, - proofing_components: proofing_components, + 'IdV: in person proofing state_id visited', + flow_path: flow_path, + step: step, + step_count: step_count, + analytics_id: analytics_id, + irs_reproofing: irs_reproofing, **extra, ) end - # A user has downloaded their backup codes - def multi_factor_auth_backup_code_download - track_event('Multi-Factor Authentication: download backup code') - end - - # A user has downloaded their personal key. This event is no longer emitted. - # @identity.idp.previous_event_name IdV: download personal key - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - def idv_personal_key_downloaded(proofing_components: nil, **extra) + # The user clicked the sp link on the "ready to verify" page + def idv_in_person_ready_to_verify_sp_link_clicked(**extra) track_event( - 'IdV: personal key downloaded', - proofing_components: proofing_components, + 'IdV: user clicked sp link on ready to verify page', **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # @param ["sms", "voice"] otp_delivery_preference + # @param [String] in_person_cta_variant Variant testing bucket label # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The user submitted their phone on the phone confirmation page - def idv_phone_confirmation_form_submitted( - success:, - otp_delivery_preference:, - errors:, - proofing_components: nil, - **extra - ) + # The user visited the "ready to verify" page for the in person proofing flow + def idv_in_person_ready_to_verify_visit(in_person_cta_variant: nil, proofing_components: nil, + **extra) track_event( - 'IdV: phone confirmation form', - success: success, - errors: errors, - otp_delivery_preference: otp_delivery_preference, + 'IdV: in person ready to verify visited', + in_person_cta_variant: in_person_cta_variant, proofing_components: proofing_components, **extra, ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The user was rate limited for submitting too many OTPs during the IDV phone step - def idv_phone_confirmation_otp_rate_limit_attempts(proofing_components: nil, **extra) + # The user clicked the what to bring link on the "ready to verify" page + def idv_in_person_ready_to_verify_what_to_bring_link_clicked(**extra) track_event( - 'Idv: Phone OTP attempts rate limited', - proofing_components: proofing_components, + 'IdV: user clicked what to bring link on ready to verify page', **extra, ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The user was locked out for hitting the phone OTP rate limit during IDV - def idv_phone_confirmation_otp_rate_limit_locked_out(proofing_components: nil, **extra) - track_event( - 'Idv: Phone OTP rate limited user', - proofing_components: proofing_components, - **extra, - ) + # @param [String] flow_path Document capture path ("hybrid" or "standard") + # The user submitted the in person proofing switch_back step + def idv_in_person_switch_back_submitted(flow_path:, **extra) + track_event('IdV: in person proofing switch_back submitted', flow_path: flow_path, **extra) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The user was rate limited for requesting too many OTPs during the IDV phone step - def idv_phone_confirmation_otp_rate_limit_sends(proofing_components: nil, **extra) - track_event( - 'Idv: Phone OTP sends rate limited', - proofing_components: proofing_components, - **extra, - ) + # @param [String] flow_path Document capture path ("hybrid" or "standard") + # The user visited the in person proofing switch_back step + def idv_in_person_switch_back_visited(flow_path:, **extra) + track_event('IdV: in person proofing switch_back visited', flow_path: flow_path, **extra) end - # @param [Boolean] success - # @param [Hash] errors - # @param ["sms","voice"] otp_delivery_preference which channel the OTP was delivered by - # @param [String] country_code country code of phone number - # @param [String] area_code area code of phone number - # @param [Boolean] rate_limit_exceeded whether or not the rate limit was exceeded by this attempt - # @param [Hash] telephony_response response from Telephony gem - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The user resent an OTP during the IDV phone step - def idv_phone_confirmation_otp_resent( - success:, - errors:, - otp_delivery_preference:, - country_code:, - area_code:, - rate_limit_exceeded:, - telephony_response:, - proofing_components: nil, + # GetUspsProofingResultsJob has completed. Includes counts of various outcomes encountered + # @param [Float] duration_seconds number of minutes the job was running + # @param [Integer] enrollments_checked number of enrollments eligible for status check + # @param [Integer] enrollments_errored number of enrollments for which we encountered an error + # @param [Integer] enrollments_expired number of enrollments which expired + # @param [Integer] enrollments_failed number of enrollments which failed identity proofing + # @param [Integer] enrollments_in_progress number of enrollments which did not have any change + # @param [Integer] enrollments_passed number of enrollments which passed identity proofing + def idv_in_person_usps_proofing_results_job_completed( + duration_seconds:, + enrollments_checked:, + enrollments_errored:, + enrollments_expired:, + enrollments_failed:, + enrollments_in_progress:, + enrollments_passed:, **extra ) track_event( - 'IdV: phone confirmation otp resent', - success: success, - errors: errors, - otp_delivery_preference: otp_delivery_preference, - country_code: country_code, - area_code: area_code, - rate_limit_exceeded: rate_limit_exceeded, - telephony_response: telephony_response, - proofing_components: proofing_components, + 'GetUspsProofingResultsJob: Job completed', + duration_seconds: duration_seconds, + enrollments_checked: enrollments_checked, + enrollments_errored: enrollments_errored, + enrollments_expired: enrollments_expired, + enrollments_failed: enrollments_failed, + enrollments_in_progress: enrollments_in_progress, + enrollments_passed: enrollments_passed, **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # @param ["sms","voice"] otp_delivery_preference which channel the OTP was delivered by - # @param [String] country_code country code of phone number - # @param [String] area_code area code of phone number - # @param [Boolean] rate_limit_exceeded whether or not the rate limit was exceeded by this attempt - # @param [String] phone_fingerprint the hmac fingerprint of the phone number formatted as e164 - # @param [Hash] telephony_response response from Telephony gem - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # @param [:test, :pinpoint] adapter which adapter the OTP was delivered with - # The user requested an OTP to confirm their phone during the IDV phone step - def idv_phone_confirmation_otp_sent( - success:, - errors:, - otp_delivery_preference:, - country_code:, - area_code:, - rate_limit_exceeded:, - phone_fingerprint:, - telephony_response:, - adapter:, - proofing_components: nil, + # Tracks exceptions that are raised when initiating deadline email in GetUspsProofingResultsJob + # @param [String] enrollment_id + # @param [String] exception_class + # @param [String] exception_message + def idv_in_person_usps_proofing_results_job_deadline_passed_email_exception( + enrollment_id:, + exception_class: nil, + exception_message: nil, **extra ) track_event( - 'IdV: phone confirmation otp sent', - success: success, - errors: errors, - otp_delivery_preference: otp_delivery_preference, - country_code: country_code, - area_code: area_code, - rate_limit_exceeded: rate_limit_exceeded, - phone_fingerprint: phone_fingerprint, - telephony_response: telephony_response, - adapter: adapter, - proofing_components: proofing_components, + 'GetUspsProofingResultsJob: Exception raised when attempting to send deadline passed email', + enrollment_id: enrollment_id, + exception_class: exception_class, + exception_message: exception_message, **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The vendor finished the process of confirming the users phone - def idv_phone_confirmation_vendor_submitted( - success:, - errors:, - proofing_components: nil, + # Tracks deadline email initiated during GetUspsProofingResultsJob + # @param [String] enrollment_id + def idv_in_person_usps_proofing_results_job_deadline_passed_email_initiated( + enrollment_id:, **extra ) track_event( - 'IdV: phone confirmation vendor', - success: success, - errors: errors, - proofing_components: proofing_components, + 'GetUspsProofingResultsJob: deadline passed email initiated', + enrollment_id: enrollment_id, **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # @param [Boolean] code_expired if the one-time code expired - # @param [Boolean] code_matches - # @param [Integer] second_factor_attempts_count number of attempts to confirm this phone - # @param [Time, nil] second_factor_locked_at timestamp when the phone was locked out - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # When a user attempts to confirm posession of a new phone number during the IDV process - def idv_phone_confirmation_otp_submitted( - success:, - errors:, - code_expired:, - code_matches:, - second_factor_attempts_count:, - second_factor_locked_at:, - proofing_components: nil, + # Tracks emails that are initiated during GetUspsProofingResultsJob + # @param [String] email_type success, failed or failed fraud + def idv_in_person_usps_proofing_results_job_email_initiated( + email_type:, **extra ) track_event( - 'IdV: phone confirmation otp submitted', - success: success, - errors: errors, - code_expired: code_expired, - code_matches: code_matches, - second_factor_attempts_count: second_factor_attempts_count, - second_factor_locked_at: second_factor_locked_at, - proofing_components: proofing_components, + 'GetUspsProofingResultsJob: Success or failure email initiated', + email_type: email_type, **extra, ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # When a user visits the page to confirm posession of a new phone number during the IDV process - def idv_phone_confirmation_otp_visit(proofing_components: nil, **extra) + # Tracks incomplete enrollments checked via the USPS API + # @param [String] enrollment_code + # @param [String] enrollment_id + # @param [Float] minutes_since_established + # @param [String] response_message + def idv_in_person_usps_proofing_results_job_enrollment_incomplete( + enrollment_code:, + enrollment_id:, + minutes_since_established:, + response_message:, + **extra + ) track_event( - 'IdV: phone confirmation otp visited', - proofing_components: proofing_components, + 'GetUspsProofingResultsJob: Enrollment incomplete', + enrollment_code: enrollment_code, + enrollment_id: enrollment_id, + minutes_since_established: minutes_since_established, + response_message: response_message, **extra, ) end - # @param ['warning','jobfail','failure'] type - # @param [Time] throttle_expires_at when the throttle expires - # @param [Integer] remaining_attempts number of attempts remaining - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # When a user gets an error during the phone finder flow of IDV - def idv_phone_error_visited( - type:, - proofing_components: nil, - throttle_expires_at: nil, - remaining_attempts: nil, + # Tracks individual enrollments that are updated during GetUspsProofingResultsJob + # @param [String] enrollment_code + # @param [String] enrollment_id + # @param [Float] minutes_since_established + # @param [Boolean] fraud_suspected + # @param [Boolean] passed did this enrollment pass or fail? + # @param [String] reason why did this enrollment pass or fail? + def idv_in_person_usps_proofing_results_job_enrollment_updated( + enrollment_code:, + enrollment_id:, + minutes_since_established:, + fraud_suspected:, + passed:, + reason:, **extra ) track_event( - 'IdV: phone error visited', - { - type: type, - proofing_components: proofing_components, - throttle_expires_at: throttle_expires_at, - remaining_attempts: remaining_attempts, - **extra, - }.compact, + 'GetUspsProofingResultsJob: Enrollment status updated', + enrollment_code: enrollment_code, + enrollment_id: enrollment_id, + minutes_since_established: minutes_since_established, + fraud_suspected: fraud_suspected, + passed: passed, + reason: reason, + **extra, ) end - # @param ["sms", "voice"] otp_delivery_preference - # @param [Boolean] success - # @param [Hash] errors - # @param [Hash] error_details - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - def idv_phone_otp_delivery_selection_submitted( - success:, - otp_delivery_preference:, - proofing_components: nil, - errors: nil, - error_details: nil, + # Tracks exceptions that are raised when running GetUspsProofingResultsJob + # @param [String] reason why was the exception raised? + # @param [String] enrollment_id + # @param [String] exception_class + # @param [String] exception_message + # @param [String] enrollment_code + # @param [Float] minutes_since_established + # @param [Float] minutes_since_last_status_check + # @param [Float] minutes_since_last_status_update + # @param [Float] minutes_to_completion + # @param [Boolean] fraud_suspected + # @param [String] primary_id_type + # @param [String] secondary_id_type + # @param [String] failure_reason + # @param [String] transaction_end_date_time + # @param [String] transaction_start_date_time + # @param [String] status + # @param [String] assurance_level + # @param [String] proofing_post_office + # @param [String] proofing_city + # @param [String] proofing_state + # @param [String] scan_count + # @param [String] response_message + # @param [Integer] response_status_code + def idv_in_person_usps_proofing_results_job_exception( + reason:, + enrollment_id:, + minutes_since_established:, + exception_class: nil, + exception_message: nil, + enrollment_code: nil, + minutes_since_last_status_check: nil, + minutes_since_last_status_update: nil, + minutes_to_completion: nil, + fraud_suspected: nil, + primary_id_type: nil, + secondary_id_type: nil, + failure_reason: nil, + transaction_end_date_time: nil, + transaction_start_date_time: nil, + status: nil, + assurance_level: nil, + proofing_post_office: nil, + proofing_city: nil, + proofing_state: nil, + scan_count: nil, + response_message: nil, + response_status_code: nil, **extra ) track_event( - 'IdV: Phone OTP Delivery Selection Submitted', - { - success: success, - errors: errors, - error_details: error_details, - otp_delivery_preference: otp_delivery_preference, - proofing_components: proofing_components, - **extra, - }.compact, + 'GetUspsProofingResultsJob: Exception raised', + reason: reason, + enrollment_id: enrollment_id, + exception_class: exception_class, + exception_message: exception_message, + enrollment_code: enrollment_code, + minutes_since_established: minutes_since_established, + minutes_since_last_status_check: minutes_since_last_status_check, + minutes_since_last_status_update: minutes_since_last_status_update, + minutes_to_completion: minutes_to_completion, + fraud_suspected: fraud_suspected, + primary_id_type: primary_id_type, + secondary_id_type: secondary_id_type, + failure_reason: failure_reason, + transaction_end_date_time: transaction_end_date_time, + transaction_start_date_time: transaction_start_date_time, + status: status, + assurance_level: assurance_level, + proofing_post_office: proofing_post_office, + proofing_city: proofing_city, + proofing_state: proofing_state, + scan_count: scan_count, + response_message: response_message, + response_status_code: response_status_code, + **extra, ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # User visited idv phone of record - def idv_phone_of_record_visited(proofing_components: nil, **extra) + # GetUspsProofingResultsJob is beginning. Includes some metadata about what the job will do + # @param [Integer] enrollments_count number of enrollments eligible for status check + # @param [Integer] reprocess_delay_minutes minimum delay since last status check + def idv_in_person_usps_proofing_results_job_started( + enrollments_count:, + reprocess_delay_minutes:, + **extra + ) track_event( - 'IdV: phone of record visited', - proofing_components: proofing_components, + 'GetUspsProofingResultsJob: Job started', + enrollments_count: enrollments_count, + reprocess_delay_minutes: reprocess_delay_minutes, **extra, ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # User visited idv phone OTP delivery selection - def idv_phone_otp_delivery_selection_visit(proofing_components: nil, **extra) + # Tracks unexpected responses from the USPS API + # @param [String] enrollment_code + # @param [String] enrollment_id + # @param [Float] minutes_since_established + # @param [String] response_message + # @param [String] reason why was this error unexpected? + def idv_in_person_usps_proofing_results_job_unexpected_response( + enrollment_code:, + enrollment_id:, + minutes_since_established:, + response_message:, + reason:, + **extra + ) track_event( - 'IdV: Phone OTP delivery Selection Visited', - proofing_components: proofing_components, + 'GetUspsProofingResultsJob: Unexpected response received', + enrollment_code: enrollment_code, + enrollment_id: enrollment_id, + minutes_since_established: minutes_since_established, + response_message: response_message, + reason: reason, + **extra, + ) + end + + # Tracks if USPS in-person proofing enrollment request fails + # @param [String] context + # @param [String] reason + # @param [Integer] enrollment_id + # @param [String] exception_class + # @param [String] exception_message + def idv_in_person_usps_request_enroll_exception( + context:, + reason:, + enrollment_id:, + exception_class:, + exception_message:, + **extra + ) + track_event( + 'USPS IPPaaS enrollment failed', + context: context, + enrollment_id: enrollment_id, + exception_class: exception_class, + exception_message: exception_message, + reason: reason, + **extra, + ) + end + + # User visits IdV + def idv_intro_visit + track_event('IdV: intro visited') + end + + # Tracks whether the user's device appears to be mobile device with a camera attached. + # @param [Boolean] is_camera_capable_mobile Whether we think the device _could_ have a camera. + # @param [Boolean,nil] camera_present Whether the user's device _actually_ has a camera available. + # @param [Integer,nil] grace_time Extra time allowed for browser to report camera availability. + # @param [Integer,nil] duration Time taken for browser to report camera availability. + def idv_mobile_device_and_camera_check( + is_camera_capable_mobile:, + camera_present: nil, + grace_time: nil, + duration: nil, + **extra + ) + track_event( + 'IdV: Mobile device and camera check', + is_camera_capable_mobile: is_camera_capable_mobile, + camera_present: camera_present, + grace_time: grace_time, + duration: duration, + **extra, + ) + end + + # @param [Integer] failed_capture_attempts Number of failed Acuant SDK attempts + # @param [Integer] failed_submission_attempts Number of failed Acuant doc submissions + # @param [String] field Image form field + # @param [String] flow_path Document capture path ("hybrid" or "standard") + # The number of acceptable failed attempts (maxFailedAttemptsBeforeNativeCamera) has been met + # or exceeded, and the system has forced the use of the native camera, rather than Acuant's + # camera, on mobile devices. + def idv_native_camera_forced( + failed_capture_attempts:, + failed_submission_attempts:, + field:, + flow_path:, + **extra + ) + track_event( + 'IdV: Native camera forced after failed attempts', + failed_capture_attempts: failed_capture_attempts, + failed_submission_attempts: failed_submission_attempts, + field: field, + flow_path: flow_path, **extra, ) end + # Tracks when user reaches verify errors due to being rejected due to fraud + def idv_not_verified_visited + track_event('IdV: Not verified visited') + end + + # Tracks if a user clicks the 'acknowledge' checkbox during personal + # key creation # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # @param [String] step the step the user was on when they clicked use a different phone number - # User decided to use a different phone number in idv - def idv_phone_use_different(step:, proofing_components: nil, **extra) + # @param [boolean] checked whether the user checked or un-checked + # the box with this click + def idv_personal_key_acknowledgment_toggled(checked:, proofing_components:, **extra) track_event( - 'IdV: use different phone number', - step: step, + 'IdV: personal key acknowledgment toggled', + checked: checked, proofing_components: proofing_components, **extra, ) end + # A user has downloaded their personal key. This event is no longer emitted. + # @identity.idp.previous_event_name IdV: download personal key # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # The system encountered an error and the proofing results are missing - def idv_proofing_resolution_result_missing(proofing_components: nil, **extra) + def idv_personal_key_downloaded(proofing_components: nil, **extra) track_event( - 'Proofing Resolution Result Missing', + 'IdV: personal key downloaded', proofing_components: proofing_components, **extra, ) end - # User submitted IDV password confirm page - # @param [Boolean] success - # @param [Boolean] fraud_review_pending - # @param [Boolean] fraud_rejection - # @param [Boolean] gpo_verification_pending # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # @param [String, nil] deactivation_reason Reason user's profile was deactivated, if any. - def idv_review_complete( - success:, + # @param [String, nil] deactivation_reason Reason profile was deactivated. + # @param [Boolean] fraud_review_pending Profile is under review for fraud + # @param [Boolean] fraud_rejection Profile is rejected due to fraud + # User submitted IDV personal key page + def idv_personal_key_submitted( fraud_review_pending:, fraud_rejection:, - gpo_verification_pending:, - deactivation_reason: nil, proofing_components: nil, + deactivation_reason: nil, **extra ) track_event( - 'IdV: review complete', - success: success, + 'IdV: personal key submitted', deactivation_reason: deactivation_reason, fraud_review_pending: fraud_review_pending, - gpo_verification_pending: gpo_verification_pending, fraud_rejection: fraud_rejection, proofing_components: proofing_components, **extra, ) end - # @param [Idv::ProofingComponentsLogging] proofing_components User's - # current proofing components - # @param [String] address_verification_method The method (phone or gpo) being - # used to verify the user's identity - # User visited IDV password confirm page - def idv_review_info_visited(proofing_components: nil, - address_verification_method: nil, - **extra) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # User visited IDV personal key page + def idv_personal_key_visited(proofing_components: nil, **extra) track_event( - 'IdV: review info visited', - address_verification_method: address_verification_method, + 'IdV: personal key visited', proofing_components: proofing_components, **extra, ) end - # @param [String] step - # @param [String] location + # @param [Boolean] success + # @param [Hash] errors + # @param ["sms", "voice"] otp_delivery_preference # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # User started over idv - def idv_start_over( - step:, - location:, + # The user submitted their phone on the phone confirmation page + def idv_phone_confirmation_form_submitted( + success:, + otp_delivery_preference:, + errors:, proofing_components: nil, **extra ) track_event( - 'IdV: start over', - step: step, - location: location, + 'IdV: phone confirmation form', + success: success, + errors: errors, + otp_delivery_preference: otp_delivery_preference, proofing_components: proofing_components, **extra, ) end - # @param [String] controller - # @param [Boolean] user_signed_in - # Authenticity token (CSRF) is invalid - def invalid_authenticity_token( - controller:, - user_signed_in: nil, - **extra - ) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # The user was rate limited for submitting too many OTPs during the IDV phone step + def idv_phone_confirmation_otp_rate_limit_attempts(proofing_components: nil, **extra) track_event( - 'Invalid Authenticity Token', - controller: controller, - user_signed_in: user_signed_in, + 'Idv: Phone OTP attempts rate limited', + proofing_components: proofing_components, **extra, ) end - # @param [String] controller - # @param [String] referer - # @param [Boolean] user_signed_in - # Redirect was almost sent to an invalid external host unexpectedly - def unsafe_redirect_error( - controller:, - referer:, - user_signed_in: nil, - **extra - ) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # The user was locked out for hitting the phone OTP rate limit during IDV + def idv_phone_confirmation_otp_rate_limit_locked_out(proofing_components: nil, **extra) track_event( - 'Unsafe Redirect', - controller: controller, - referer: referer, - user_signed_in: user_signed_in, - **extra, - ) - end - - # @param [Integer] rendered_event_count how many events were rendered in the API response - # @param [Boolean] authenticated whether the request was successfully authenticated - # @param [Float] elapsed_time the amount of time the function took to run - # @param [Boolean] success - # An IRS Attempt API client has requested events - def irs_attempts_api_events( - rendered_event_count:, - authenticated:, - elapsed_time:, - success:, - **extra - ) - track_event( - 'IRS Attempt API: Events submitted', - rendered_event_count: rendered_event_count, - authenticated: authenticated, - elapsed_time: elapsed_time, - success: success, + 'Idv: Phone OTP rate limited user', + proofing_components: proofing_components, **extra, ) end - # @param [String] event_type - # @param [Integer] unencrypted_payload_num_bytes size of payload as JSON data - # @param [Boolean] recorded if the full event was recorded or not - def irs_attempts_api_event_metadata( - event_type:, - unencrypted_payload_num_bytes:, - recorded:, - **extra - ) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # The user was rate limited for requesting too many OTPs during the IDV phone step + def idv_phone_confirmation_otp_rate_limit_sends(proofing_components: nil, **extra) track_event( - 'IRS Attempt API: Event metadata', - event_type: event_type, - unencrypted_payload_num_bytes: unencrypted_payload_num_bytes, - recorded: recorded, + 'Idv: Phone OTP sends rate limited', + proofing_components: proofing_components, **extra, ) end # @param [Boolean] success - # @param [String] client_id - # @param [Boolean] client_id_parameter_present - # @param [Boolean] id_token_hint_parameter_present - # @param [Boolean] sp_initiated - # @param [Boolean] oidc - # @param [Boolean] saml_request_valid # @param [Hash] errors - # @param [Hash] error_details - # @param [String] method - # Logout Initiated - def logout_initiated( - success: nil, - client_id: nil, - sp_initiated: nil, - oidc: nil, - client_id_parameter_present: nil, - id_token_hint_parameter_present: nil, - saml_request_valid: nil, - errors: nil, - error_details: nil, - method: nil, + # @param ["sms","voice"] otp_delivery_preference which channel the OTP was delivered by + # @param [String] country_code country code of phone number + # @param [String] area_code area code of phone number + # @param [Boolean] rate_limit_exceeded whether or not the rate limit was exceeded by this attempt + # @param [Hash] telephony_response response from Telephony gem + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # The user resent an OTP during the IDV phone step + def idv_phone_confirmation_otp_resent( + success:, + errors:, + otp_delivery_preference:, + country_code:, + area_code:, + rate_limit_exceeded:, + telephony_response:, + proofing_components: nil, **extra ) track_event( - 'Logout Initiated', + 'IdV: phone confirmation otp resent', success: success, - client_id: client_id, - client_id_parameter_present: client_id_parameter_present, - id_token_hint_parameter_present: id_token_hint_parameter_present, errors: errors, - error_details: error_details, - sp_initiated: sp_initiated, - oidc: oidc, - saml_request_valid: saml_request_valid, - method: method, + otp_delivery_preference: otp_delivery_preference, + country_code: country_code, + area_code: area_code, + rate_limit_exceeded: rate_limit_exceeded, + telephony_response: telephony_response, + proofing_components: proofing_components, **extra, ) end # @param [Boolean] success - # @param [String] client_id - # @param [Boolean] client_id_parameter_present - # @param [Boolean] id_token_hint_parameter_present - # @param [Boolean] sp_initiated - # @param [Boolean] oidc - # @param [Boolean] saml_request_valid # @param [Hash] errors - # @param [Hash] error_details - # @param [String] method - # OIDC Logout Requested - def oidc_logout_requested( - success: nil, - client_id: nil, - sp_initiated: nil, - oidc: nil, - client_id_parameter_present: nil, - id_token_hint_parameter_present: nil, - saml_request_valid: nil, - errors: nil, - error_details: nil, - method: nil, + # @param ["sms","voice"] otp_delivery_preference which channel the OTP was delivered by + # @param [String] country_code country code of phone number + # @param [String] area_code area code of phone number + # @param [Boolean] rate_limit_exceeded whether or not the rate limit was exceeded by this attempt + # @param [String] phone_fingerprint the hmac fingerprint of the phone number formatted as e164 + # @param [Hash] telephony_response response from Telephony gem + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [:test, :pinpoint] adapter which adapter the OTP was delivered with + # The user requested an OTP to confirm their phone during the IDV phone step + def idv_phone_confirmation_otp_sent( + success:, + errors:, + otp_delivery_preference:, + country_code:, + area_code:, + rate_limit_exceeded:, + phone_fingerprint:, + telephony_response:, + adapter:, + proofing_components: nil, **extra ) track_event( - 'OIDC Logout Requested', + 'IdV: phone confirmation otp sent', success: success, - client_id: client_id, - client_id_parameter_present: client_id_parameter_present, - id_token_hint_parameter_present: id_token_hint_parameter_present, errors: errors, - error_details: error_details, - sp_initiated: sp_initiated, - oidc: oidc, - saml_request_valid: saml_request_valid, - method: method, + otp_delivery_preference: otp_delivery_preference, + country_code: country_code, + area_code: area_code, + rate_limit_exceeded: rate_limit_exceeded, + phone_fingerprint: phone_fingerprint, + telephony_response: telephony_response, + adapter: adapter, + proofing_components: proofing_components, **extra, ) end # @param [Boolean] success - # @param [String] client_id - # @param [Boolean] client_id_parameter_present - # @param [Boolean] id_token_hint_parameter_present - # @param [Boolean] sp_initiated - # @param [Boolean] oidc - # @param [Boolean] saml_request_valid # @param [Hash] errors - # @param [Hash] error_details - # @param [String] method - # OIDC Logout Visited - def oidc_logout_visited( - success: nil, - client_id: nil, - sp_initiated: nil, - oidc: nil, - client_id_parameter_present: nil, - id_token_hint_parameter_present: nil, - saml_request_valid: nil, - errors: nil, - error_details: nil, - method: nil, + # @param [Boolean] code_expired if the one-time code expired + # @param [Boolean] code_matches + # @param [Integer] second_factor_attempts_count number of attempts to confirm this phone + # @param [Time, nil] second_factor_locked_at timestamp when the phone was locked out + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # When a user attempts to confirm posession of a new phone number during the IDV process + def idv_phone_confirmation_otp_submitted( + success:, + errors:, + code_expired:, + code_matches:, + second_factor_attempts_count:, + second_factor_locked_at:, + proofing_components: nil, **extra ) track_event( - 'OIDC Logout Page Visited', + 'IdV: phone confirmation otp submitted', success: success, - client_id: client_id, - client_id_parameter_present: client_id_parameter_present, - id_token_hint_parameter_present: id_token_hint_parameter_present, errors: errors, - error_details: error_details, - sp_initiated: sp_initiated, - oidc: oidc, - saml_request_valid: saml_request_valid, - method: method, + code_expired: code_expired, + code_matches: code_matches, + second_factor_attempts_count: second_factor_attempts_count, + second_factor_locked_at: second_factor_locked_at, + proofing_components: proofing_components, + **extra, + ) + end + + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # When a user visits the page to confirm posession of a new phone number during the IDV process + def idv_phone_confirmation_otp_visit(proofing_components: nil, **extra) + track_event( + 'IdV: phone confirmation otp visited', + proofing_components: proofing_components, **extra, ) end # @param [Boolean] success - # @param [String] client_id - # @param [Boolean] client_id_parameter_present - # @param [Boolean] id_token_hint_parameter_present - # @param [Boolean] sp_initiated - # @param [Boolean] oidc - # @param [Boolean] saml_request_valid # @param [Hash] errors - # @param [Hash] error_details - # @param [String] method - # OIDC Logout Submitted - def oidc_logout_submitted( - success: nil, - client_id: nil, - sp_initiated: nil, - oidc: nil, - client_id_parameter_present: nil, - id_token_hint_parameter_present: nil, - saml_request_valid: nil, - errors: nil, - error_details: nil, - method: nil, + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # The vendor finished the process of confirming the users phone + def idv_phone_confirmation_vendor_submitted( + success:, + errors:, + proofing_components: nil, **extra ) track_event( - 'OIDC Logout Submitted', + 'IdV: phone confirmation vendor', success: success, - client_id: client_id, - client_id_parameter_present: client_id_parameter_present, - id_token_hint_parameter_present: id_token_hint_parameter_present, errors: errors, - error_details: error_details, - sp_initiated: sp_initiated, - oidc: oidc, - saml_request_valid: saml_request_valid, - method: method, + proofing_components: proofing_components, **extra, ) end - # @param [Boolean] success Whether authentication was successful - # @param [Hash] errors Authentication error reasons, if unsuccessful - # @param [String] context - # @param [String] multi_factor_auth_method - # @param [Integer] auth_app_configuration_id - # @param [Integer] piv_cac_configuration_id - # @param [Integer] key_id - # @param [Integer] webauthn_configuration_id - # @param [Integer] phone_configuration_id - # @param [Boolean] confirmation_for_add_phone - # @param [String] area_code - # @param [String] country_code - # @param [String] phone_fingerprint the hmac fingerprint of the phone number formatted as e164 - # Multi-Factor Authentication - def multi_factor_auth( - success:, - errors: nil, - context: nil, - multi_factor_auth_method: nil, - auth_app_configuration_id: nil, - piv_cac_configuration_id: nil, - key_id: nil, - webauthn_configuration_id: nil, - confirmation_for_add_phone: nil, - phone_configuration_id: nil, - pii_like_keypaths: nil, - area_code: nil, - country_code: nil, - phone_fingerprint: nil, + # @param ['warning','jobfail','failure'] type + # @param [Time] throttle_expires_at when the throttle expires + # @param [Integer] remaining_attempts number of attempts remaining + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # When a user gets an error during the phone finder flow of IDV + def idv_phone_error_visited( + type:, + proofing_components: nil, + throttle_expires_at: nil, + remaining_attempts: nil, **extra ) track_event( - 'Multi-Factor Authentication', - success: success, - errors: errors, - context: context, - multi_factor_auth_method: multi_factor_auth_method, - auth_app_configuration_id: auth_app_configuration_id, - piv_cac_configuration_id: piv_cac_configuration_id, - key_id: key_id, - webauthn_configuration_id: webauthn_configuration_id, - confirmation_for_add_phone: confirmation_for_add_phone, - phone_configuration_id: phone_configuration_id, - pii_like_keypaths: pii_like_keypaths, - area_code: area_code, - country_code: country_code, - phone_fingerprint: phone_fingerprint, - **extra, - ) - end - - # Tracks when the the user has added the MFA method phone to their account - # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user - def multi_factor_auth_added_phone(enabled_mfa_methods_count:, **extra) - track_event( - 'Multi-Factor Authentication: Added phone', + 'IdV: phone error visited', { - method_name: :phone, - enabled_mfa_methods_count: enabled_mfa_methods_count, + type: type, + proofing_components: proofing_components, + throttle_expires_at: throttle_expires_at, + remaining_attempts: remaining_attempts, **extra, }.compact, ) end - # Tracks when the user has added the MFA method piv_cac to their account - # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user - def multi_factor_auth_added_piv_cac(enabled_mfa_methods_count:, **extra) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # User visited idv phone of record + def idv_phone_of_record_visited(proofing_components: nil, **extra) track_event( - 'Multi-Factor Authentication: Added PIV_CAC', - { - method_name: :piv_cac, - enabled_mfa_methods_count: enabled_mfa_methods_count, - **extra, - }.compact, + 'IdV: phone of record visited', + proofing_components: proofing_components, + **extra, ) end - # Tracks when the user has added the MFA method TOTP to their account - # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user - def multi_factor_auth_added_totp(enabled_mfa_methods_count:, **extra) + # @param ["sms", "voice"] otp_delivery_preference + # @param [Boolean] success + # @param [Hash] errors + # @param [Hash] error_details + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + def idv_phone_otp_delivery_selection_submitted( + success:, + otp_delivery_preference:, + proofing_components: nil, + errors: nil, + error_details: nil, + **extra + ) track_event( - 'Multi-Factor Authentication: Added TOTP', + 'IdV: Phone OTP Delivery Selection Submitted', { - method_name: :totp, - enabled_mfa_methods_count: enabled_mfa_methods_count, + success: success, + errors: errors, + error_details: error_details, + otp_delivery_preference: otp_delivery_preference, + proofing_components: proofing_components, **extra, }.compact, ) end - # Tracks when the user has added the MFA method webauthn to their account - # @param [Boolean] platform_authenticator indicates if webauthn_platform was used - # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user - def multi_factor_auth_added_webauthn( - platform_authenticator:, - enabled_mfa_methods_count:, **extra - ) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # User visited idv phone OTP delivery selection + def idv_phone_otp_delivery_selection_visit(proofing_components: nil, **extra) track_event( - 'Multi-Factor Authentication: Added webauthn', - { - method_name: :webauthn, - platform_authenticator: platform_authenticator, - enabled_mfa_methods_count: enabled_mfa_methods_count, - **extra, - }.compact, + 'IdV: Phone OTP delivery Selection Visited', + proofing_components: proofing_components, + **extra, ) end - # Tracks when the user visits the backup code confirmation setup page - # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user - def multi_factor_auth_enter_backup_code_confirmation_visit( - enabled_mfa_methods_count:, **extra - ) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String] step the step the user was on when they clicked use a different phone number + # User decided to use a different phone number in idv + def idv_phone_use_different(step:, proofing_components: nil, **extra) track_event( - 'Multi-Factor Authentication: enter backup code confirmation visited', - { - enabled_mfa_methods_count: enabled_mfa_methods_count, - **extra, - }.compact, + 'IdV: use different phone number', + step: step, + proofing_components: proofing_components, + **extra, ) end - # @param ["authentication","reauthentication","confirmation"] context user session context - # User visited the page to enter a backup code as their MFA - def multi_factor_auth_enter_backup_code_visit(context:, **extra) + # @identity.idp.previous_event_name IdV: Verify setup errors visited + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # Tracks when the user reaches the verify please call page after failing proofing + def idv_please_call_visited(proofing_components: nil, **extra) track_event( - 'Multi-Factor Authentication: enter backup code visited', - context: context, + 'IdV: Verify please call visited', + proofing_components: proofing_components, **extra, ) end - # @param ["authentication","reauthentication","confirmation"] context user session context - # User visited the page to enter a personal key as their mfa (legacy flow) - def multi_factor_auth_enter_personal_key_visit(context:, **extra) + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # The system encountered an error and the proofing results are missing + def idv_proofing_resolution_result_missing(proofing_components: nil, **extra) track_event( - 'Multi-Factor Authentication: enter personal key visited', - context: context, + 'Proofing Resolution Result Missing', + proofing_components: proofing_components, **extra, ) end - # @param ["authentication","reauthentication","confirmation"] context user session context - # @param ["piv_cac"] multi_factor_auth_method - # @param [Integer, nil] piv_cac_configuration_id PIV/CAC configuration database ID - # User used a PIV/CAC as their mfa - def multi_factor_auth_enter_piv_cac( - context:, - multi_factor_auth_method:, - piv_cac_configuration_id:, + # User submitted IDV password confirm page + # @param [Boolean] success + # @param [Boolean] fraud_review_pending + # @param [Boolean] fraud_rejection + # @param [Boolean] gpo_verification_pending + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # @param [String, nil] deactivation_reason Reason user's profile was deactivated, if any. + def idv_review_complete( + success:, + fraud_review_pending:, + fraud_rejection:, + gpo_verification_pending:, + deactivation_reason: nil, + proofing_components: nil, **extra ) track_event( - 'Multi-Factor Authentication: enter PIV CAC visited', - context: context, - multi_factor_auth_method: multi_factor_auth_method, - piv_cac_configuration_id: piv_cac_configuration_id, + 'IdV: review complete', + success: success, + deactivation_reason: deactivation_reason, + fraud_review_pending: fraud_review_pending, + gpo_verification_pending: gpo_verification_pending, + fraud_rejection: fraud_rejection, + proofing_components: proofing_components, **extra, ) end - # @param [String] context - # @param [String] multi_factor_auth_method - # @param [Boolean] confirmation_for_add_phone - # @param [Integer] phone_configuration_id - # Multi-Factor Authentication enter OTP visited - def multi_factor_auth_enter_otp_visit( - context:, - multi_factor_auth_method:, - confirmation_for_add_phone:, - phone_configuration_id:, - **extra - ) + # @param [Idv::ProofingComponentsLogging] proofing_components User's + # current proofing components + # @param [String] address_verification_method The method (phone or gpo) being + # used to verify the user's identity + # User visited IDV password confirm page + def idv_review_info_visited(proofing_components: nil, + address_verification_method: nil, + **extra) track_event( - 'Multi-Factor Authentication: enter OTP visited', - context: context, - multi_factor_auth_method: multi_factor_auth_method, - confirmation_for_add_phone: confirmation_for_add_phone, - phone_configuration_id: phone_configuration_id, + 'IdV: review info visited', + address_verification_method: address_verification_method, + proofing_components: proofing_components, **extra, ) end - # @param ["authentication","reauthentication","confirmation"] context user session context - # User visited the page to enter a TOTP as their mfa - def multi_factor_auth_enter_totp_visit(context:, **extra) - track_event('Multi-Factor Authentication: enter TOTP visited', context: context, **extra) - end - - # @param ["authentication","reauthentication","confirmation"] context user session context - # @param ["webauthn","webauthn_platform"] multi_factor_auth_method which webauthn method was used, - # webauthn means a roaming authenticator like a yubikey, webauthn_platform means a platform - # authenticator like face or touch ID - # @param [Integer, nil] webauthn_configuration_id webauthn database ID - # User visited the page to authenticate with webauthn (yubikey, face ID or touch ID) - def multi_factor_auth_enter_webauthn_visit( - context:, - multi_factor_auth_method:, - webauthn_configuration_id:, + # Tracks when the user visits one of the the session error pages. + # @param [String] type + # @param [Integer,nil] attempts_remaining + def idv_session_error_visited( + type:, + attempts_remaining: nil, **extra ) track_event( - 'Multi-Factor Authentication: enter webAuthn authentication visited', - context: context, - multi_factor_auth_method: multi_factor_auth_method, - webauthn_configuration_id: webauthn_configuration_id, + 'IdV: session error visited', + type: type, + attempts_remaining: attempts_remaining, **extra, ) end - # Max multi factor auth attempts met - def multi_factor_auth_max_attempts - track_event('Multi-Factor Authentication: max attempts reached') - end - - # Multi factor selected from auth options list - # @param [Boolean] success - # @param [Hash] errors - # @param [String] selection - def multi_factor_auth_option_list(success:, errors:, selection:, **extra) + # @param [String] step + # @param [String] location + # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components + # User started over idv + def idv_start_over( + step:, + location:, + proofing_components: nil, + **extra + ) track_event( - 'Multi-Factor Authentication: option list', - success: success, - errors: errors, - selection: selection, + 'IdV: start over', + step: step, + location: location, + proofing_components: proofing_components, **extra, ) end - # User visited the list of multi-factor options to use - def multi_factor_auth_option_list_visit - track_event('Multi-Factor Authentication: option list visited') + # @param [String] flow_path Document capture path ("hybrid" or "standard") + # @param [String] in_person_cta_variant Variant testing bucket label + # The user clicked the troubleshooting option to start in-person proofing + def idv_verify_in_person_troubleshooting_option_clicked(flow_path:, in_person_cta_variant:, + **extra) + track_event( + 'IdV: verify in person troubleshooting option clicked', + flow_path: flow_path, + in_person_cta_variant: in_person_cta_variant, + **extra, + ) end - # Multi factor auth phone setup - # @param [Boolean] success - # @param [Hash] errors - # @param [String] otp_delivery_preference - # @param [String] area_code - # @param [String] carrier - # @param [String] country_code - # @param [String] phone_type - # @param [Hash] types - def multi_factor_auth_phone_setup(success:, - errors:, - otp_delivery_preference:, - area_code:, - carrier:, - country_code:, - phone_type:, - types:, - **extra) - + # @param [String] controller + # @param [Boolean] user_signed_in + # Authenticity token (CSRF) is invalid + def invalid_authenticity_token( + controller:, + user_signed_in: nil, + **extra + ) track_event( - 'Multi-Factor Authentication: phone setup', - success: success, - errors: errors, - otp_delivery_preference: otp_delivery_preference, - area_code: area_code, - carrier: carrier, - country_code: country_code, - phone_type: phone_type, - types: types, + 'Invalid Authenticity Token', + controller: controller, + user_signed_in: user_signed_in, **extra, ) end - # Max multi factor max otp sends reached - def multi_factor_auth_max_sends - track_event('Multi-Factor Authentication: max otp sends reached') + # @param [String] event_type + # @param [Integer] unencrypted_payload_num_bytes size of payload as JSON data + # @param [Boolean] recorded if the full event was recorded or not + def irs_attempts_api_event_metadata( + event_type:, + unencrypted_payload_num_bytes:, + recorded:, + **extra + ) + track_event( + 'IRS Attempt API: Event metadata', + event_type: event_type, + unencrypted_payload_num_bytes: unencrypted_payload_num_bytes, + recorded: recorded, + **extra, + ) end - # Tracks when a user sets up a multi factor auth method - # @param [Boolean] success Whether authenticator setup was successful - # @param [Hash] errors Authenticator setup error reasons, if unsuccessful - # @param [String] multi_factor_auth_method - # @param [Boolean] in_multi_mfa_selection_flow - # @param [integer] enabled_mfa_methods_count - def multi_factor_auth_setup( + # @param [Integer] rendered_event_count how many events were rendered in the API response + # @param [Boolean] authenticated whether the request was successfully authenticated + # @param [Float] elapsed_time the amount of time the function took to run + # @param [Boolean] success + # An IRS Attempt API client has requested events + def irs_attempts_api_events( + rendered_event_count:, + authenticated:, + elapsed_time:, success:, - multi_factor_auth_method:, - enabled_mfa_methods_count:, - in_multi_mfa_selection_flow:, - errors: nil, **extra ) track_event( - 'Multi-Factor Authentication Setup', + 'IRS Attempt API: Events submitted', + rendered_event_count: rendered_event_count, + authenticated: authenticated, + elapsed_time: elapsed_time, success: success, - errors: errors, - multi_factor_auth_method: multi_factor_auth_method, - in_multi_mfa_selection_flow: in_multi_mfa_selection_flow, - enabled_mfa_methods_count: enabled_mfa_methods_count, **extra, ) end - # Tracks when an openid connect bearer token authentication request is made # @param [Boolean] success - # @param [Integer] ial - # @param [String] client_id Service Provider issuer + # @param [String] client_id + # @param [Boolean] client_id_parameter_present + # @param [Boolean] id_token_hint_parameter_present + # @param [Boolean] sp_initiated + # @param [Boolean] oidc + # @param [Boolean] saml_request_valid # @param [Hash] errors - def openid_connect_bearer_token(success:, ial:, client_id:, errors:, **extra) + # @param [Hash] error_details + # @param [String] method + # Logout Initiated + def logout_initiated( + success: nil, + client_id: nil, + sp_initiated: nil, + oidc: nil, + client_id_parameter_present: nil, + id_token_hint_parameter_present: nil, + saml_request_valid: nil, + errors: nil, + error_details: nil, + method: nil, + **extra + ) track_event( - 'OpenID Connect: bearer token authentication', + 'Logout Initiated', success: success, - ial: ial, client_id: client_id, + client_id_parameter_present: client_id_parameter_present, + id_token_hint_parameter_present: id_token_hint_parameter_present, errors: errors, + error_details: error_details, + sp_initiated: sp_initiated, + oidc: oidc, + saml_request_valid: saml_request_valid, + method: method, **extra, ) end - # Tracks when openid authorization request is made - # @param [String] client_id - # @param [String] scope - # @param [Array] acr_values - # @param [Boolean] unauthorized_scope - # @param [Boolean] user_fully_authenticated - # @param [String] code_digest hash of returned "code" param - def openid_connect_request_authorization( - client_id:, - scope:, - acr_values:, - unauthorized_scope:, - user_fully_authenticated:, - code_digest:, + # @param [Boolean] success Whether authentication was successful + # @param [Hash] errors Authentication error reasons, if unsuccessful + # @param [String] context + # @param [String] multi_factor_auth_method + # @param [Integer] auth_app_configuration_id + # @param [Integer] piv_cac_configuration_id + # @param [Integer] key_id + # @param [Integer] webauthn_configuration_id + # @param [Integer] phone_configuration_id + # @param [Boolean] confirmation_for_add_phone + # @param [String] area_code + # @param [String] country_code + # @param [String] phone_fingerprint the hmac fingerprint of the phone number formatted as e164 + # Multi-Factor Authentication + def multi_factor_auth( + success:, + errors: nil, + context: nil, + multi_factor_auth_method: nil, + auth_app_configuration_id: nil, + piv_cac_configuration_id: nil, + key_id: nil, + webauthn_configuration_id: nil, + confirmation_for_add_phone: nil, + phone_configuration_id: nil, + pii_like_keypaths: nil, + area_code: nil, + country_code: nil, + phone_fingerprint: nil, **extra ) track_event( - 'OpenID Connect: authorization request', - client_id: client_id, - scope: scope, - acr_values: acr_values, - unauthorized_scope: unauthorized_scope, - user_fully_authenticated: user_fully_authenticated, - code_digest: code_digest, + 'Multi-Factor Authentication', + success: success, + errors: errors, + context: context, + multi_factor_auth_method: multi_factor_auth_method, + auth_app_configuration_id: auth_app_configuration_id, + piv_cac_configuration_id: piv_cac_configuration_id, + key_id: key_id, + webauthn_configuration_id: webauthn_configuration_id, + confirmation_for_add_phone: confirmation_for_add_phone, + phone_configuration_id: phone_configuration_id, + pii_like_keypaths: pii_like_keypaths, + area_code: area_code, + country_code: country_code, + phone_fingerprint: phone_fingerprint, **extra, ) end - # Tracks when an openid connect token request is made - # @param [String] client_id - # @param [String] user_id - # @param [String] code_digest hash of "code" param - def openid_connect_token(client_id:, user_id:, code_digest:, **extra) + # Tracks when the the user has added the MFA method phone to their account + # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user + def multi_factor_auth_added_phone(enabled_mfa_methods_count:, **extra) track_event( - 'OpenID Connect: token', - client_id: client_id, - user_id: user_id, - code_digest: code_digest, - **extra, + 'Multi-Factor Authentication: Added phone', + { + method_name: :phone, + enabled_mfa_methods_count: enabled_mfa_methods_count, + **extra, + }.compact, ) end - # Tracks when user is redirected to OTP expired page - # @param [String] otp_sent_at - # @param [String] otp_expiration - def otp_expired_visited(otp_sent_at:, otp_expiration:, **extra) + # Tracks when the user has added the MFA method piv_cac to their account + # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user + def multi_factor_auth_added_piv_cac(enabled_mfa_methods_count:, **extra) track_event( - 'OTP Expired Page Visited', - otp_sent_at: otp_sent_at, - otp_expiration: otp_expiration, - **extra, - ) - end - - # Tracks if otp phone validation failed - # @identity.idp.previous_event_name Twilio Phone Validation Failed - # @param [String] error - # @param [String] context - # @param [String] country - def otp_phone_validation_failed(error:, context:, country:, **extra) - track_event( - 'Vendor Phone Validation failed', - error: error, - context: context, - country: country, - **extra, + 'Multi-Factor Authentication: Added PIV_CAC', + { + method_name: :piv_cac, + enabled_mfa_methods_count: enabled_mfa_methods_count, + **extra, + }.compact, ) end - # User has been marked as authenticated - # @param [String] authentication_type - def user_marked_authed(authentication_type:, **extra) + # Tracks when the user has added the MFA method TOTP to their account + # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user + def multi_factor_auth_added_totp(enabled_mfa_methods_count:, **extra) track_event( - 'User marked authenticated', - authentication_type: authentication_type, - **extra, + 'Multi-Factor Authentication: Added TOTP', + { + method_name: :totp, + enabled_mfa_methods_count: enabled_mfa_methods_count, + **extra, + }.compact, ) end - # User has attempted to access an action that requires re-authenticating - # @param [String] auth_method - # @param [String] authenticated_at - def user_2fa_reauthentication_required(auth_method:, authenticated_at:, **extra) + # Tracks when the user has added the MFA method webauthn to their account + # @param [Boolean] platform_authenticator indicates if webauthn_platform was used + # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user + def multi_factor_auth_added_webauthn( + platform_authenticator:, + enabled_mfa_methods_count:, **extra + ) track_event( - 'User 2FA Reauthentication Required', - auth_method: auth_method, - authenticated_at: authenticated_at, - **extra, + 'Multi-Factor Authentication: Added webauthn', + { + method_name: :webauthn, + platform_authenticator: platform_authenticator, + enabled_mfa_methods_count: enabled_mfa_methods_count, + **extra, + }.compact, ) end - # User registration has been handed off to agency page - # @param [Boolean] ial2 - # @param [Integer] ialmax - # @param [String] service_provider_name - # @param [String] page_occurence - # @param [String] needs_completion_screen_reason - # @param [Array] sp_request_requested_attributes - # @param [Array] sp_session_requested_attributes - def user_registration_agency_handoff_page_visit( - ial2:, - service_provider_name:, - page_occurence:, - needs_completion_screen_reason:, - sp_session_requested_attributes:, - sp_request_requested_attributes: nil, - ialmax: nil, - **extra + # A user has downloaded their backup codes + def multi_factor_auth_backup_code_download + track_event('Multi-Factor Authentication: download backup code') + end + + # Tracks when the user visits the backup code confirmation setup page + # @param [Integer] enabled_mfa_methods_count number of registered mfa methods for the user + def multi_factor_auth_enter_backup_code_confirmation_visit( + enabled_mfa_methods_count:, **extra + ) + track_event( + 'Multi-Factor Authentication: enter backup code confirmation visited', + { + enabled_mfa_methods_count: enabled_mfa_methods_count, + **extra, + }.compact, ) + end + # @param ["authentication","reauthentication","confirmation"] context user session context + # User visited the page to enter a backup code as their MFA + def multi_factor_auth_enter_backup_code_visit(context:, **extra) track_event( - 'User registration: agency handoff visited', - ial2: ial2, - ialmax: ialmax, - service_provider_name: service_provider_name, - page_occurence: page_occurence, - needs_completion_screen_reason: needs_completion_screen_reason, - sp_request_requested_attributes: sp_request_requested_attributes, - sp_session_requested_attributes: sp_session_requested_attributes, + 'Multi-Factor Authentication: enter backup code visited', + context: context, **extra, ) end - # Tracks when user makes an otp delivery selection - # @param [String] otp_delivery_preference (sms or voice) - # @param [Boolean] resend - # @param [String] country_code - # @param [String] area_code - # @param ["authentication","reauthentication","confirmation"] context user session context - # @param [Hash] pii_like_keypaths - def otp_delivery_selection( - otp_delivery_preference:, - resend:, - country_code:, - area_code:, + # @param [String] context + # @param [String] multi_factor_auth_method + # @param [Boolean] confirmation_for_add_phone + # @param [Integer] phone_configuration_id + # Multi-Factor Authentication enter OTP visited + def multi_factor_auth_enter_otp_visit( context:, - pii_like_keypaths:, + multi_factor_auth_method:, + confirmation_for_add_phone:, + phone_configuration_id:, **extra ) track_event( - 'OTP: Delivery Selection', - otp_delivery_preference: otp_delivery_preference, - resend: resend, - country_code: country_code, - area_code: area_code, + 'Multi-Factor Authentication: enter OTP visited', context: context, - pii_like_keypaths: pii_like_keypaths, + multi_factor_auth_method: multi_factor_auth_method, + confirmation_for_add_phone: confirmation_for_add_phone, + phone_configuration_id: phone_configuration_id, **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # The user updated their password - def password_changed(success:, errors:, **extra) - track_event('Password Changed', success: success, errors: errors, **extra) - end - - # @param [Boolean] success - # @param [Hash] errors - # The user added a password after verifying their email for account creation - def password_creation(success:, errors:, **extra) - track_event('Password Creation', success: success, errors: errors, **extra) - end - - # The user got their password incorrect the max number of times, their session was terminated - def password_max_attempts - track_event('Password Max Attempts Reached') - end - - # @param [Boolean] success - # @param [Hash] errors - # @param [Boolean, nil] confirmed if the account the reset is being requested for has a - # confirmed email - # @param [Boolean, nil] active_profile if the account the reset is being requested for has an - # active proofed profile - # The user entered an email address to request a password reset - def password_reset_email(success:, errors:, confirmed:, active_profile:, **extra) + # @param ["authentication","reauthentication","confirmation"] context user session context + # User visited the page to enter a personal key as their mfa (legacy flow) + def multi_factor_auth_enter_personal_key_visit(context:, **extra) track_event( - 'Password Reset: Email Submitted', - success: success, - errors: errors, - confirmed: confirmed, - active_profile: active_profile, + 'Multi-Factor Authentication: enter personal key visited', + context: context, **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # @param [Boolean] profile_deactivated if the active profile for the account was deactivated - # (the user will need to use their personal key to reactivate their profile) - # The user changed the password for their account via the password reset flow - def password_reset_password(success:, errors:, profile_deactivated:, **extra) + # @param ["authentication","reauthentication","confirmation"] context user session context + # @param ["piv_cac"] multi_factor_auth_method + # @param [Integer, nil] piv_cac_configuration_id PIV/CAC configuration database ID + # User used a PIV/CAC as their mfa + def multi_factor_auth_enter_piv_cac( + context:, + multi_factor_auth_method:, + piv_cac_configuration_id:, + **extra + ) track_event( - 'Password Reset: Password Submitted', - success: success, - errors: errors, - profile_deactivated: profile_deactivated, + 'Multi-Factor Authentication: enter PIV CAC visited', + context: context, + multi_factor_auth_method: multi_factor_auth_method, + piv_cac_configuration_id: piv_cac_configuration_id, **extra, ) end - # User has visited the page that lets them confirm if they want a new personal key - def profile_personal_key_visit - track_event('Profile: Visited new personal key') + # @param ["authentication","reauthentication","confirmation"] context user session context + # User visited the page to enter a TOTP as their mfa + def multi_factor_auth_enter_totp_visit(context:, **extra) + track_event('Multi-Factor Authentication: enter TOTP visited', context: context, **extra) end - # @param [Boolean] success - # @param [Hash] errors - # @param [String] user_id UUID of the user to receive password token - # A password token has been sent for user - def password_reset_token(success:, errors:, user_id:, **extra) + # @param ["authentication","reauthentication","confirmation"] context user session context + # @param ["webauthn","webauthn_platform"] multi_factor_auth_method which webauthn method was used, + # webauthn means a roaming authenticator like a yubikey, webauthn_platform means a platform + # authenticator like face or touch ID + # @param [Integer, nil] webauthn_configuration_id webauthn database ID + # User visited the page to authenticate with webauthn (yubikey, face ID or touch ID) + def multi_factor_auth_enter_webauthn_visit( + context:, + multi_factor_auth_method:, + webauthn_configuration_id:, + **extra + ) track_event( - 'Password Reset: Token Submitted', - success: success, - errors: errors, - user_id: user_id, + 'Multi-Factor Authentication: enter webAuthn authentication visited', + context: context, + multi_factor_auth_method: multi_factor_auth_method, + webauthn_configuration_id: webauthn_configuration_id, **extra, ) end - # Password reset form has been visited. - def password_reset_visit - track_event('Password Reset: Email Form Visited') - end - - # Pending account reset cancelled - def pending_account_reset_cancelled - track_event('Pending account reset cancelled') + # Max multi factor auth attempts met + def multi_factor_auth_max_attempts + track_event('Multi-Factor Authentication: max attempts reached') end - # Pending account reset visited - def pending_account_reset_visited - track_event('Pending account reset visited') + # Max multi factor max otp sends reached + def multi_factor_auth_max_sends + track_event('Multi-Factor Authentication: max otp sends reached') end + # Multi factor selected from auth options list # @param [Boolean] success # @param [Hash] errors - # Alert user if a personal key was used to sign in - def personal_key_alert_about_sign_in(success:, errors:, **extra) + # @param [String] selection + def multi_factor_auth_option_list(success:, errors:, selection:, **extra) track_event( - 'Personal key: Alert user about sign in', + 'Multi-Factor Authentication: option list', success: success, errors: errors, + selection: selection, **extra, ) end - # Account reactivated with personal key - def personal_key_reactivation - track_event('Personal key reactivation: Account reactivated with personal key') - end - - # Account reactivated with personal key as MFA - def personal_key_reactivation_sign_in - track_event( - 'Personal key reactivation: Account reactivated with personal key as MFA', - ) + # User visited the list of multi-factor options to use + def multi_factor_auth_option_list_visit + track_event('Multi-Factor Authentication: option list visited') end + # Multi factor auth phone setup # @param [Boolean] success # @param [Hash] errors - # @param [Hash] pii_like_keypaths - # Personal key form submitted - def personal_key_reactivation_submitted(success:, errors:, pii_like_keypaths:, **extra) + # @param [String] otp_delivery_preference + # @param [String] area_code + # @param [String] carrier + # @param [String] country_code + # @param [String] phone_type + # @param [Hash] types + def multi_factor_auth_phone_setup(success:, + errors:, + otp_delivery_preference:, + area_code:, + carrier:, + country_code:, + phone_type:, + types:, + **extra) track_event( - 'Personal key reactivation: Personal key form submitted', + 'Multi-Factor Authentication: phone setup', success: success, errors: errors, - pii_like_keypaths: pii_like_keypaths, + otp_delivery_preference: otp_delivery_preference, + area_code: area_code, + carrier: carrier, + country_code: country_code, + phone_type: phone_type, + types: types, **extra, ) end - # Personal key reactivation visited - def personal_key_reactivation_visited - track_event('Personal key reactivation: Personal key form visited') - end - - # @param [Boolean] personal_key_present if personal key is present - # Personal key viewed - def personal_key_viewed(personal_key_present:, **extra) + # Tracks when a user sets up a multi factor auth method + # @param [Boolean] success Whether authenticator setup was successful + # @param [Hash] errors Authenticator setup error reasons, if unsuccessful + # @param [String] multi_factor_auth_method + # @param [Boolean] in_multi_mfa_selection_flow + # @param [integer] enabled_mfa_methods_count + def multi_factor_auth_setup( + success:, + multi_factor_auth_method:, + enabled_mfa_methods_count:, + in_multi_mfa_selection_flow:, + errors: nil, + **extra + ) track_event( - 'Personal key viewed', - personal_key_present: personal_key_present, + 'Multi-Factor Authentication Setup', + success: success, + errors: errors, + multi_factor_auth_method: multi_factor_auth_method, + in_multi_mfa_selection_flow: in_multi_mfa_selection_flow, + enabled_mfa_methods_count: enabled_mfa_methods_count, **extra, ) end # @param [Boolean] success + # @param [String] client_id + # @param [Boolean] client_id_parameter_present + # @param [Boolean] id_token_hint_parameter_present + # @param [Boolean] sp_initiated + # @param [Boolean] oidc + # @param [Boolean] saml_request_valid # @param [Hash] errors - # @param [String] delivery_preference - # @param [Integer] phone_configuration_id - # @param [Boolean] make_default_number - # User has submitted a change in phone number - def phone_change_submitted( - success:, - errors:, - delivery_preference:, - phone_configuration_id:, - make_default_number:, + # @param [Hash] error_details + # @param [String] method + # OIDC Logout Requested + def oidc_logout_requested( + success: nil, + client_id: nil, + sp_initiated: nil, + oidc: nil, + client_id_parameter_present: nil, + id_token_hint_parameter_present: nil, + saml_request_valid: nil, + errors: nil, + error_details: nil, + method: nil, **extra ) track_event( - 'Phone Number Change: Form submitted', + 'OIDC Logout Requested', success: success, + client_id: client_id, + client_id_parameter_present: client_id_parameter_present, + id_token_hint_parameter_present: id_token_hint_parameter_present, errors: errors, - delivery_preference: delivery_preference, - phone_configuration_id: phone_configuration_id, - make_default_number: make_default_number, + error_details: error_details, + sp_initiated: sp_initiated, + oidc: oidc, + saml_request_valid: saml_request_valid, + method: method, **extra, ) end - # User has viewed the page to change their phone number - def phone_change_viewed - track_event('Phone Number Change: Visited') - end - # @param [Boolean] success - # @param [Integer] phone_configuration_id - # tracks a phone number deletion event - def phone_deletion(success:, phone_configuration_id:, **extra) + # @param [String] client_id + # @param [Boolean] client_id_parameter_present + # @param [Boolean] id_token_hint_parameter_present + # @param [Boolean] sp_initiated + # @param [Boolean] oidc + # @param [Boolean] saml_request_valid + # @param [Hash] errors + # @param [Hash] error_details + # @param [String] method + # OIDC Logout Submitted + def oidc_logout_submitted( + success: nil, + client_id: nil, + sp_initiated: nil, + oidc: nil, + client_id_parameter_present: nil, + id_token_hint_parameter_present: nil, + saml_request_valid: nil, + errors: nil, + error_details: nil, + method: nil, + **extra + ) track_event( - 'Phone Number Deletion: Submitted', + 'OIDC Logout Submitted', success: success, - phone_configuration_id: phone_configuration_id, + client_id: client_id, + client_id_parameter_present: client_id_parameter_present, + id_token_hint_parameter_present: id_token_hint_parameter_present, + errors: errors, + error_details: error_details, + sp_initiated: sp_initiated, + oidc: oidc, + saml_request_valid: saml_request_valid, + method: method, **extra, ) end # @param [Boolean] success + # @param [String] client_id + # @param [Boolean] client_id_parameter_present + # @param [Boolean] id_token_hint_parameter_present + # @param [Boolean] sp_initiated + # @param [Boolean] oidc + # @param [Boolean] saml_request_valid # @param [Hash] errors - # tracks piv cac login event - def piv_cac_login(success:, errors:, **extra) + # @param [Hash] error_details + # @param [String] method + # OIDC Logout Visited + def oidc_logout_visited( + success: nil, + client_id: nil, + sp_initiated: nil, + oidc: nil, + client_id_parameter_present: nil, + id_token_hint_parameter_present: nil, + saml_request_valid: nil, + errors: nil, + error_details: nil, + method: nil, + **extra + ) track_event( - 'PIV/CAC Login', + 'OIDC Logout Page Visited', success: success, + client_id: client_id, + client_id_parameter_present: client_id_parameter_present, + id_token_hint_parameter_present: id_token_hint_parameter_present, errors: errors, + error_details: error_details, + sp_initiated: sp_initiated, + oidc: oidc, + saml_request_valid: saml_request_valid, + method: method, **extra, ) end - # @param [String] error - # Tracks if a Profile encryption is invalid - def profile_encryption_invalid(error:, **extra) - track_event('Profile Encryption: Invalid', error: error, **extra) + # Tracks when an openid connect bearer token authentication request is made + # @param [Boolean] success + # @param [Integer] ial + # @param [String] client_id Service Provider issuer + # @param [Hash] errors + def openid_connect_bearer_token(success:, ial:, client_id:, errors:, **extra) + track_event( + 'OpenID Connect: bearer token authentication', + success: success, + ial: ial, + client_id: client_id, + errors: errors, + **extra, + ) end - # @see #profile_personal_key_create_notifications - # User has chosen to receive a new personal key - def profile_personal_key_create - track_event('Profile: Created new personal key') + # Tracks when openid authorization request is made + # @param [String] client_id + # @param [String] scope + # @param [Array] acr_values + # @param [Boolean] unauthorized_scope + # @param [Boolean] user_fully_authenticated + # @param [String] code_digest hash of returned "code" param + def openid_connect_request_authorization( + client_id:, + scope:, + acr_values:, + unauthorized_scope:, + user_fully_authenticated:, + code_digest:, + **extra + ) + track_event( + 'OpenID Connect: authorization request', + client_id: client_id, + scope: scope, + acr_values: acr_values, + unauthorized_scope: unauthorized_scope, + user_fully_authenticated: user_fully_authenticated, + code_digest: code_digest, + **extra, + ) end - # @param [true] success this event always succeeds - # @param [Integer] emails number of email addresses the notification was sent to - # @param [Array] sms_message_ids AWS Pinpoint SMS message IDs for each phone number that - # was notified - # User has chosen to receive a new personal key, contains stats about notifications that - # were sent to phone numbers and email addresses for the user - def profile_personal_key_create_notifications(success:, emails:, sms_message_ids:, **extra) + # Tracks when an openid connect token request is made + # @param [String] client_id + # @param [String] user_id + # @param [String] code_digest hash of "code" param + def openid_connect_token(client_id:, user_id:, code_digest:, **extra) track_event( - 'Profile: Created new personal key notifications', - success: success, - emails: emails, - sms_message_ids: sms_message_ids, + 'OpenID Connect: token', + client_id: client_id, + user_id: user_id, + code_digest: code_digest, **extra, ) end - # @identity.idp.previous_event_name Proofing Address Timeout - # The job for address verification (PhoneFinder) did not record a result in the expected - # place during the expected time frame - def proofing_address_result_missing - track_event('Proofing Address Result Missing') - end - - # @identity.idp.previous_event_name Proofing Document Timeout - # The job for document authentication did not record a result in the expected - # place during the expected time frame - def proofing_document_result_missing - track_event('Proofing Document Result Missing') - end - - # Rate limit triggered - # @param [String] type - def rate_limit_triggered(type:, **extra) - track_event('Rate Limit Triggered', type: type, **extra) - end - - # The result of a reCAPTCHA verification request was received - # @param [Hash] recaptcha_result Full reCAPTCHA response body - # @param [Float] score_threshold Minimum value for considering passing result - # @param [Boolean] evaluated_as_valid Whether result was considered valid - # @param [String] validator_class Class name of validator - # @param [String, nil] exception_class Class name of exception, if error occurred - def recaptcha_verify_result_received( - recaptcha_result:, - score_threshold:, - evaluated_as_valid:, - validator_class:, - exception_class:, + # Tracks when user makes an otp delivery selection + # @param [String] otp_delivery_preference (sms or voice) + # @param [Boolean] resend + # @param [String] country_code + # @param [String] area_code + # @param ["authentication","reauthentication","confirmation"] context user session context + # @param [Hash] pii_like_keypaths + def otp_delivery_selection( + otp_delivery_preference:, + resend:, + country_code:, + area_code:, + context:, + pii_like_keypaths:, **extra ) track_event( - 'reCAPTCHA verify result received', - recaptcha_result:, - score_threshold:, - evaluated_as_valid:, - validator_class:, - exception_class:, + 'OTP: Delivery Selection', + otp_delivery_preference: otp_delivery_preference, + resend: resend, + country_code: country_code, + area_code: area_code, + context: context, + pii_like_keypaths: pii_like_keypaths, **extra, ) end - # User authenticated by a remembered device - # @param [DateTime] cookie_created_at time the remember device cookie was created - # @param [Integer] cookie_age_seconds age of the cookie in seconds - def remembered_device_used_for_authentication( - cookie_created_at:, - cookie_age_seconds:, - **extra - ) + # Tracks when user is redirected to OTP expired page + # @param [String] otp_sent_at + # @param [String] otp_expiration + def otp_expired_visited(otp_sent_at:, otp_expiration:, **extra) track_event( - 'Remembered device used for authentication', - cookie_created_at: cookie_created_at, - cookie_age_seconds: cookie_age_seconds, + 'OTP Expired Page Visited', + otp_sent_at: otp_sent_at, + otp_expiration: otp_expiration, **extra, ) end - # Service provider initiated remote logout - # @param [String] service_provider - # @param [Boolean] saml_request_valid - def remote_logout_initiated( - service_provider:, - saml_request_valid:, - **extra - ) + # Tracks if otp phone validation failed + # @identity.idp.previous_event_name Twilio Phone Validation Failed + # @param [String] error + # @param [String] context + # @param [String] country + def otp_phone_validation_failed(error:, context:, country:, **extra) track_event( - 'Remote Logout initiated', - service_provider: service_provider, - saml_request_valid: saml_request_valid, + 'Vendor Phone Validation failed', + error: error, + context: context, + country: country, **extra, ) end - # Service provider completed remote logout - # @param [String] service_provider - # @param [String] user_id - def remote_logout_completed( - service_provider:, - user_id:, - **extra - ) - track_event( - 'Remote Logout completed', - service_provider: service_provider, - user_id: user_id, - **extra, - ) + # @param [Boolean] success + # @param [Hash] errors + # The user updated their password + def password_changed(success:, errors:, **extra) + track_event('Password Changed', success: success, errors: errors, **extra) end - # A response timed out - # @param [String] backtrace - # @param [String] exception_message - # @param [String] exception_class - def response_timed_out( - backtrace:, - exception_message:, - exception_class:, - **extra - ) - track_event( - 'Response Timed Out', - backtrace: backtrace, - exception_message: exception_message, - exception_class: exception_class, - **extra, - ) + # @param [Boolean] success + # @param [Hash] errors + # The user added a password after verifying their email for account creation + def password_creation(success:, errors:, **extra) + track_event('Password Creation', success: success, errors: errors, **extra) end - # User cancelled the process and returned to the sp - # @param [String] redirect_url the url of the service provider - # @param [String] flow - # @param [String] step - # @param [String] location - def return_to_sp_cancelled( - redirect_url:, - step: nil, - location: nil, - flow: nil, - **extra - ) - track_event( - 'Return to SP: Cancelled', - redirect_url: redirect_url, - step: step, - location: location, - flow: flow, - **extra, - ) + # The user got their password incorrect the max number of times, their session was terminated + def password_max_attempts + track_event('Password Max Attempts Reached') end - # Tracks when a user is redirected back to the service provider after failing to proof. - # @param [String] redirect_url the url of the service provider - # @param [String] flow - # @param [String] step - # @param [String] location - def return_to_sp_failure_to_proof(redirect_url:, flow: nil, step: nil, location: nil, **extra) + # @param [Boolean] success + # @param [Hash] errors + # @param [Boolean, nil] confirmed if the account the reset is being requested for has a + # confirmed email + # @param [Boolean, nil] active_profile if the account the reset is being requested for has an + # active proofed profile + # The user entered an email address to request a password reset + def password_reset_email(success:, errors:, confirmed:, active_profile:, **extra) track_event( - 'Return to SP: Failed to proof', - redirect_url: redirect_url, - flow: flow, - step: step, - location: location, + 'Password Reset: Email Submitted', + success: success, + errors: errors, + confirmed: confirmed, + active_profile: active_profile, **extra, ) end - # Tracks when rules of use is visited - def rules_of_use_visit - track_event('Rules of Use Visited') - end - - # Tracks when rules of use is submitted with a success or failure # @param [Boolean] success # @param [Hash] errors - def rules_of_use_submitted(success: nil, errors: nil, **extra) + # @param [Boolean] profile_deactivated if the active profile for the account was deactivated + # (the user will need to use their personal key to reactivate their profile) + # The user changed the password for their account via the password reset flow + def password_reset_password(success:, errors:, profile_deactivated:, **extra) track_event( - 'Rules of Use Submitted', + 'Password Reset: Password Submitted', success: success, errors: errors, + profile_deactivated: profile_deactivated, **extra, ) end - # Tracks when security event is received # @param [Boolean] success - # @param [String] error_code # @param [Hash] errors - # @param [String] jti - # @param [String] user_id - # @param [String] client_id - def security_event_received( - success:, - error_code: nil, - errors: nil, - jti: nil, - user_id: nil, - client_id: nil, - **extra - ) + # @param [String] user_id UUID of the user to receive password token + # A password token has been sent for user + def password_reset_token(success:, errors:, user_id:, **extra) track_event( - 'RISC: Security event received', + 'Password Reset: Token Submitted', success: success, - error_code: error_code, errors: errors, - jti: jti, user_id: user_id, - client_id: client_id, **extra, ) end - # Tracks when a user is bounced back from the service provider due to an integration issue. - def sp_handoff_bounced_detected - track_event('SP handoff bounced detected') + # Password reset form has been visited. + def password_reset_visit + track_event('Password Reset: Email Form Visited') end - # Tracks when a user visits the bounced page. - def sp_handoff_bounced_visit - track_event('SP handoff bounced visited') + # Pending account reset cancelled + def pending_account_reset_cancelled + track_event('Pending account reset cancelled') end - # Tracks when a user visits the "This agency no longer uses Login.gov" page. - def sp_inactive_visit - track_event('SP inactive visited') + # Pending account reset visited + def pending_account_reset_visited + track_event('Pending account reset visited') end - # Tracks when a user is redirected back to the service provider - # @param [Integer] ial - # @param [Integer] billed_ial - def sp_redirect_initiated(ial:, billed_ial:, **extra) + # @param [Boolean] success + # @param [Hash] errors + # Alert user if a personal key was used to sign in + def personal_key_alert_about_sign_in(success:, errors:, **extra) track_event( - 'SP redirect initiated', - ial: ial, - billed_ial: billed_ial, + 'Personal key: Alert user about sign in', + success: success, + errors: errors, **extra, ) end - # Tracks when a user triggered a rate limit throttle - # @param [String] throttle_type - def throttler_rate_limit_triggered(throttle_type:, **extra) - track_event( - 'Throttler Rate Limit Triggered', - throttle_type: throttle_type, - **extra, - ) + # Account reactivated with personal key + def personal_key_reactivation + track_event('Personal key reactivation: Account reactivated with personal key') end - # Tracks when a user visits TOTP device setup - # @param [Boolean] user_signed_up - # @param [Boolean] totp_secret_present - # @param [Integer] enabled_mfa_methods_count - def totp_setup_visit( - user_signed_up:, - totp_secret_present:, - enabled_mfa_methods_count:, - **extra - ) + # Account reactivated with personal key as MFA + def personal_key_reactivation_sign_in track_event( - 'TOTP Setup Visited', - user_signed_up: user_signed_up, - totp_secret_present: totp_secret_present, - enabled_mfa_methods_count: enabled_mfa_methods_count, - **extra, + 'Personal key reactivation: Account reactivated with personal key as MFA', ) end - # Tracks when a user disabled a TOTP device - def totp_user_disabled - track_event('TOTP: User Disabled') - end - - # Tracks when service provider consent is revoked - # @param [String] issuer issuer of the service provider consent to be revoked - def sp_revoke_consent_revoked(issuer:, **extra) + # @param [Boolean] success + # @param [Hash] errors + # @param [Hash] pii_like_keypaths + # Personal key form submitted + def personal_key_reactivation_submitted(success:, errors:, pii_like_keypaths:, **extra) track_event( - 'SP Revoke Consent: Revoked', - issuer: issuer, + 'Personal key reactivation: Personal key form submitted', + success: success, + errors: errors, + pii_like_keypaths: pii_like_keypaths, **extra, ) end - # Tracks when the page to revoke consent (unlink from) a service provider visited - # @param [String] issuer which issuer - def sp_revoke_consent_visited(issuer:, **extra) + # Personal key reactivation visited + def personal_key_reactivation_visited + track_event('Personal key reactivation: Personal key form visited') + end + + # @param [Boolean] personal_key_present if personal key is present + # Personal key viewed + def personal_key_viewed(personal_key_present:, **extra) track_event( - 'SP Revoke Consent: Visited', - issuer: issuer, + 'Personal key viewed', + personal_key_present: personal_key_present, **extra, ) end - # Record SAML authentication payload Hash # @param [Boolean] success # @param [Hash] errors - # @param [String] nameid_format - # @param [Array] authn_context - # @param [String] authn_context_comparison - # @param [String] service_provider - def saml_auth( + # @param [String] delivery_preference + # @param [Integer] phone_configuration_id + # @param [Boolean] make_default_number + # User has submitted a change in phone number + def phone_change_submitted( success:, errors:, - nameid_format:, - authn_context:, - authn_context_comparison:, - service_provider:, + delivery_preference:, + phone_configuration_id:, + make_default_number:, **extra ) track_event( - 'SAML Auth', + 'Phone Number Change: Form submitted', success: success, errors: errors, - nameid_format: nameid_format, - authn_context: authn_context, - authn_context_comparison: authn_context_comparison, - service_provider: service_provider, + delivery_preference: delivery_preference, + phone_configuration_id: phone_configuration_id, + make_default_number: make_default_number, **extra, ) end - # @param [Integer] requested_ial - # @param [String,nil] requested_aal_authn_context - # @param [Boolean,nil] force_authn - # @param [String] service_provider - # An external request for SAML Authentication was received - def saml_auth_request( - requested_ial:, - requested_aal_authn_context:, - force_authn:, - service_provider:, - **extra - ) - track_event( - 'SAML Auth Request', - { - requested_ial: requested_ial, - requested_aal_authn_context: requested_aal_authn_context, - force_authn: force_authn, - service_provider: service_provider, - **extra, - }.compact, - ) - end - - # tracks if the session is kept alive - def session_kept_alive - track_event('Session Kept Alive') - end - - # tracks if the session timed out - def session_timed_out - track_event('Session Timed Out') - end - - # tracks when a user's session is timed out - def session_total_duration_timeout - track_event('User Maximum Session Length Exceeded') + # User has viewed the page to change their phone number + def phone_change_viewed + track_event('Phone Number Change: Visited') end - # @param [String] flash - # @param [String] stored_location - # @param [String] sign_in_a_b_test_bucket - # tracks when a user visits the sign in page - def sign_in_page_visit(flash:, stored_location:, sign_in_a_b_test_bucket:, **extra) + # @param [Boolean] success + # @param [Integer] phone_configuration_id + # tracks a phone number deletion event + def phone_deletion(success:, phone_configuration_id:, **extra) track_event( - 'Sign in page visited', - flash: flash, - stored_location: stored_location, - sign_in_a_b_test_bucket:, + 'Phone Number Deletion: Submitted', + success: success, + phone_configuration_id: phone_configuration_id, **extra, ) end # @param [Boolean] success - # @param [Boolean] new_user - # @param [Boolean] has_other_auth_methods - # @param [Integer] phone_configuration_id - # tracks when a user opts into SMS - def sms_opt_in_submitted( - success:, - new_user:, - has_other_auth_methods:, - phone_configuration_id:, - **extra - ) + # @param [Hash] errors + # tracks piv cac login event + def piv_cac_login(success:, errors:, **extra) track_event( - 'SMS Opt-In: Submitted', + 'PIV/CAC Login', success: success, - new_user: new_user, - has_other_auth_methods: has_other_auth_methods, - phone_configuration_id: phone_configuration_id, + errors: errors, **extra, ) end - # @param [Boolean] new_user - # @param [Boolean] has_other_auth_methods - # @param [Integer] phone_configuration_id - # tracks when a user visits the sms opt in page - def sms_opt_in_visit( - new_user:, - has_other_auth_methods:, - phone_configuration_id:, - **extra - ) + # @param [String] redirect_url URL user was directed to + # @param [String, nil] step which step + # @param [String, nil] location which part of a step, if applicable + # @param ["idv", String, nil] flow which flow + # User was redirected to the login.gov policy page + def policy_redirect(redirect_url:, step: nil, location: nil, flow: nil, **extra) track_event( - 'SMS Opt-In: Visited', - new_user: new_user, - has_other_auth_methods: has_other_auth_methods, - phone_configuration_id: phone_configuration_id, + 'Policy Page Redirect', + redirect_url: redirect_url, + step: step, + location: location, + flow: flow, **extra, ) end - # @param [String] area_code - # @param [String] country_code - # @param [String] phone_fingerprint the hmac fingerprint of the phone number formatted as e164 - # @param [String] context the context of the OTP, either "authentication" for confirmed phones - # or "confirmation" for unconfirmed - # @param ["sms","voice"] otp_delivery_preference the channel used to send the message - # @param [Boolean] resend - # @param [Hash] telephony_response - # @param [:test, :pinpoint] adapter which adapter the OTP was delivered with - # @param [Boolean] success - # A phone one-time password send was attempted - def telephony_otp_sent( - area_code:, - country_code:, - phone_fingerprint:, - context:, - otp_delivery_preference:, - resend:, - telephony_response:, - adapter:, - success:, - **extra - ) - track_event( - 'Telephony: OTP sent', - { - area_code: area_code, - country_code: country_code, - phone_fingerprint: phone_fingerprint, - context: context, - otp_delivery_preference: otp_delivery_preference, - resend: resend, - telephony_response: telephony_response, - adapter: adapter, - success: success, - **extra, - }, - ) + # @param [String] error + # Tracks if a Profile encryption is invalid + def profile_encryption_invalid(error:, **extra) + track_event('Profile Encryption: Invalid', error: error, **extra) end - # Tracks When users visit the add phone page - def add_phone_setup_visit + # @see #profile_personal_key_create_notifications + # User has chosen to receive a new personal key + def profile_personal_key_create + track_event('Profile: Created new personal key') + end + + # @param [true] success this event always succeeds + # @param [Integer] emails number of email addresses the notification was sent to + # @param [Array] sms_message_ids AWS Pinpoint SMS message IDs for each phone number that + # was notified + # User has chosen to receive a new personal key, contains stats about notifications that + # were sent to phone numbers and email addresses for the user + def profile_personal_key_create_notifications(success:, emails:, sms_message_ids:, **extra) track_event( - 'Phone Setup Visited', + 'Profile: Created new personal key notifications', + success: success, + emails: emails, + sms_message_ids: sms_message_ids, + **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # @param [String] sign_up_mfa_priority_bucket - # Tracks when the the user has selected and submitted additional MFA methods on user registration - def user_registration_2fa_additional_setup(success:, - sign_up_mfa_priority_bucket:, - errors: nil, - **extra) - track_event( - 'User Registration: Additional 2FA Setup', - { - success: success, - sign_up_mfa_priority_bucket: sign_up_mfa_priority_bucket, - errors: errors, - **extra, - }.compact, - ) + # User has visited the page that lets them confirm if they want a new personal key + def profile_personal_key_visit + track_event('Profile: Visited new personal key') end - # Tracks when user visits additional MFA selection page - # @param [String] sign_up_mfa_priority_bucket - def user_registration_2fa_additional_setup_visit(sign_up_mfa_priority_bucket:, **extra) + # @identity.idp.previous_event_name Proofing Address Timeout + # The job for address verification (PhoneFinder) did not record a result in the expected + # place during the expected time frame + def proofing_address_result_missing + track_event('Proofing Address Result Missing') + end + + # @identity.idp.previous_event_name Proofing Document Timeout + # The job for document authentication did not record a result in the expected + # place during the expected time frame + def proofing_document_result_missing + track_event('Proofing Document Result Missing') + end + + # Rate limit triggered + # @param [String] type + def rate_limit_triggered(type:, **extra) + track_event('Rate Limit Triggered', type: type, **extra) + end + + # The result of a reCAPTCHA verification request was received + # @param [Hash] recaptcha_result Full reCAPTCHA response body + # @param [Float] score_threshold Minimum value for considering passing result + # @param [Boolean] evaluated_as_valid Whether result was considered valid + # @param [String] validator_class Class name of validator + # @param [String, nil] exception_class Class name of exception, if error occurred + def recaptcha_verify_result_received( + recaptcha_result:, + score_threshold:, + evaluated_as_valid:, + validator_class:, + exception_class:, + **extra + ) track_event( - 'User Registration: Additional 2FA Setup visited', - sign_up_mfa_priority_bucket: sign_up_mfa_priority_bucket, + 'reCAPTCHA verify result received', + recaptcha_result:, + score_threshold:, + evaluated_as_valid:, + validator_class:, + exception_class:, **extra, ) end - # @param [Boolean] success - # @param [Hash] errors - # @param [Integer] enabled_mfa_methods_count - # @param [Integer] selected_mfa_count - # @param ['voice', 'auth_app'] selection - # @param [String] sign_up_mfa_priority_bucket - # Tracks when the the user has selected and submitted MFA auth methods on user registration - def user_registration_2fa_setup( - success:, - sign_up_mfa_priority_bucket:, - errors: nil, - selected_mfa_count: nil, - enabled_mfa_methods_count: nil, - selection: nil, + # User authenticated by a remembered device + # @param [DateTime] cookie_created_at time the remember device cookie was created + # @param [Integer] cookie_age_seconds age of the cookie in seconds + def remembered_device_used_for_authentication( + cookie_created_at:, + cookie_age_seconds:, **extra ) track_event( - 'User Registration: 2FA Setup', - { - success: success, - errors: errors, - selected_mfa_count: selected_mfa_count, - enabled_mfa_methods_count: enabled_mfa_methods_count, - sign_up_mfa_priority_bucket: sign_up_mfa_priority_bucket, - selection: selection, - **extra, - }.compact, + 'Remembered device used for authentication', + cookie_created_at: cookie_created_at, + cookie_age_seconds: cookie_age_seconds, + **extra, ) end - # @param [String] mfa_method - # Tracks when the the user fully registered by submitting their first MFA method into the system - def user_registration_user_fully_registered( - mfa_method:, + # Service provider completed remote logout + # @param [String] service_provider + # @param [String] user_id + def remote_logout_completed( + service_provider:, + user_id:, **extra ) track_event( - 'User Registration: User Fully Registered', - { - mfa_method: mfa_method, - **extra, - }.compact, + 'Remote Logout completed', + service_provider: service_provider, + user_id: user_id, + **extra, ) end - # @param [Boolean] success - # @param [Hash] mfa_method_counts - # @param [Integer] enabled_mfa_methods_count - # @param [Hash] pii_like_keypaths - # Tracks when a user has completed MFA setup - def user_registration_mfa_setup_complete( - success:, - mfa_method_counts:, - enabled_mfa_methods_count:, - pii_like_keypaths:, + # Service provider initiated remote logout + # @param [String] service_provider + # @param [Boolean] saml_request_valid + def remote_logout_initiated( + service_provider:, + saml_request_valid:, **extra ) track_event( - 'User Registration: MFA Setup Complete', - { - success: success, - mfa_method_counts: mfa_method_counts, - enabled_mfa_methods_count: enabled_mfa_methods_count, - pii_like_keypaths: pii_like_keypaths, - **extra, - }.compact, + 'Remote Logout initiated', + service_provider: service_provider, + saml_request_valid: saml_request_valid, + **extra, ) end - # Tracks when user's piv cac is disabled - def user_registration_piv_cac_disabled - track_event('User Registration: piv cac disabled') - end - - # Tracks when user's piv cac setup - def user_registration_piv_cac_setup_visit(**extra) + # @param [Boolean] success + # Tracks request for resending confirmation for new emails to an account + def resend_add_email_request(success:, **extra) track_event( - 'User Registration: piv cac setup visited', + 'Resend Add Email Requested', + success: success, **extra, ) end - # Tracks when user visits Suggest Another MFA Page - def user_registration_suggest_another_mfa_notice_visited - track_event('User Registration: Suggest Another MFA Notice visited') - end - - # Tracks when user skips Suggest Another MFA Page - def user_registration_suggest_another_mfa_notice_skipped - track_event('User Registration: Suggest Another MFA Notice Skipped') - end - - # Tracks when user visits MFA selection page - # @param [String] sign_up_mfa_priority_bucket - def user_registration_2fa_setup_visit(sign_up_mfa_priority_bucket:, **extra) + # A response timed out + # @param [String] backtrace + # @param [String] exception_message + # @param [String] exception_class + def response_timed_out( + backtrace:, + exception_message:, + exception_class:, + **extra + ) track_event( - 'User Registration: 2FA Setup visited', - sign_up_mfa_priority_bucket: sign_up_mfa_priority_bucket, + 'Response Timed Out', + backtrace: backtrace, + exception_message: exception_message, + exception_class: exception_class, **extra, ) end - # @param [Hash] vendor_status - # @param [String,nil] redirect_from - # Tracks when vendor has outage - def vendor_outage( - vendor_status:, - redirect_from: nil, + # User cancelled the process and returned to the sp + # @param [String] redirect_url the url of the service provider + # @param [String] flow + # @param [String] step + # @param [String] location + def return_to_sp_cancelled( + redirect_url:, + step: nil, + location: nil, + flow: nil, **extra ) track_event( - 'Vendor Outage', - redirect_from: redirect_from, - vendor_status: vendor_status, + 'Return to SP: Cancelled', + redirect_url: redirect_url, + step: step, + location: location, + flow: flow, **extra, ) end - # @param [Boolean] success - # @param [Integer] mfa_method_counts - # Tracks when WebAuthn is deleted - def webauthn_deleted(success:, mfa_method_counts:, pii_like_keypaths:, **extra) + # Tracks when a user is redirected back to the service provider after failing to proof. + # @param [String] redirect_url the url of the service provider + # @param [String] flow + # @param [String] step + # @param [String] location + def return_to_sp_failure_to_proof(redirect_url:, flow: nil, step: nil, location: nil, **extra) track_event( - 'WebAuthn Deleted', - success: success, - mfa_method_counts: mfa_method_counts, - pii_like_keypaths: pii_like_keypaths, + 'Return to SP: Failed to proof', + redirect_url: redirect_url, + flow: flow, + step: step, + location: location, **extra, ) end - # @param [Hash] platform_authenticator - # @param [Hash] errors - # @param [Integer] enabled_mfa_methods_count + # Tracks when rules of use is submitted with a success or failure # @param [Boolean] success - # Tracks when WebAuthn setup is visited - def webauthn_setup_visit(platform_authenticator:, errors:, enabled_mfa_methods_count:, success:, - **extra) + # @param [Hash] errors + def rules_of_use_submitted(success: nil, errors: nil, **extra) track_event( - 'WebAuthn Setup Visited', - platform_authenticator: platform_authenticator, - errors: errors, - enabled_mfa_methods_count: enabled_mfa_methods_count, + 'Rules of Use Submitted', success: success, + errors: errors, **extra, ) end - # Tracks when user visits enter email page - # @param [String] sign_in_a_b_test_bucket - # @param [Boolean] from_sign_in - def user_registration_enter_email_visit(sign_in_a_b_test_bucket:, from_sign_in:, **extra) - track_event( - 'User Registration: enter email visited', - sign_in_a_b_test_bucket:, - from_sign_in:, - **extra, - ) + # Tracks when rules of use is visited + def rules_of_use_visit + track_event('Rules of Use Visited') end - # @param [Integer] enabled_mfa_methods_count - # Tracks when user visits the phone setup step during registration - def user_registration_phone_setup_visit(enabled_mfa_methods_count:, **extra) - track_event( - 'User Registration: phone setup visited', - enabled_mfa_methods_count: enabled_mfa_methods_count, - **extra, - ) - end - - # Tracks when user cancels registration - # @param [String] request_came_from the controller/action the request came from - def user_registration_cancellation(request_came_from:, **extra) - track_event( - 'User registration: cancellation visited', - request_came_from: request_came_from, - **extra, - ) - end - - # Tracks when user completes registration - # @param [Boolean] ial2 - # @param [Boolean] ialmax - # @param [String] service_provider_name - # @param [String] page_occurence - # @param [String] needs_completion_screen_reason - # @param [String] sign_in_a_b_test_bucket - # @param [Array] sp_request_requested_attributes - # @param [Array] sp_session_requested_attributes - def user_registration_complete( - ial2:, - service_provider_name:, - page_occurence:, - needs_completion_screen_reason:, - sign_in_a_b_test_bucket:, - sp_session_requested_attributes:, - sp_request_requested_attributes: nil, - ialmax: nil, + # Record SAML authentication payload Hash + # @param [Boolean] success + # @param [Hash] errors + # @param [String] nameid_format + # @param [Array] authn_context + # @param [String] authn_context_comparison + # @param [String] service_provider + def saml_auth( + success:, + errors:, + nameid_format:, + authn_context:, + authn_context_comparison:, + service_provider:, **extra ) track_event( - 'User registration: complete', - ial2: ial2, - ialmax: ialmax, - service_provider_name: service_provider_name, - page_occurence: page_occurence, - needs_completion_screen_reason: needs_completion_screen_reason, - sign_in_a_b_test_bucket:, - sp_request_requested_attributes: sp_request_requested_attributes, - sp_session_requested_attributes: sp_session_requested_attributes, + 'SAML Auth', + success: success, + errors: errors, + nameid_format: nameid_format, + authn_context: authn_context, + authn_context_comparison: authn_context_comparison, + service_provider: service_provider, **extra, ) end - # Tracks when user submits registration email - # @param [Boolean] success - # @param [Boolean] throttled - # @param [Hash] errors - # @param [Hash] error_details - # @param [String] user_id - # @param [String] domain_name - def user_registration_email( - success:, - throttled:, - errors:, - error_details: nil, - user_id: nil, - domain_name: nil, + # @param [Integer] requested_ial + # @param [String,nil] requested_aal_authn_context + # @param [Boolean,nil] force_authn + # @param [String] service_provider + # An external request for SAML Authentication was received + def saml_auth_request( + requested_ial:, + requested_aal_authn_context:, + force_authn:, + service_provider:, **extra ) track_event( - 'User Registration: Email Submitted', + 'SAML Auth Request', { - success: success, - throttled: throttled, - errors: errors, - error_details: error_details, - user_id: user_id, - domain_name: domain_name, + requested_ial: requested_ial, + requested_aal_authn_context: requested_aal_authn_context, + force_authn: force_authn, + service_provider: service_provider, **extra, }.compact, ) end - # Tracks when user confirms registration email + # Tracks when security event is received # @param [Boolean] success + # @param [String] error_code # @param [Hash] errors - # @param [Hash] error_details + # @param [String] jti # @param [String] user_id - def user_registration_email_confirmation( + # @param [String] client_id + def security_event_received( success:, - errors:, - error_details: nil, + error_code: nil, + errors: nil, + jti: nil, user_id: nil, + client_id: nil, **extra ) track_event( - 'User Registration: Email Confirmation', + 'RISC: Security event received', success: success, + error_code: error_code, errors: errors, - error_details: error_details, + jti: jti, user_id: user_id, + client_id: client_id, **extra, ) end - # Tracks if request to get address candidates from ArcGIS fails - # @param [String] exception_class - # @param [String] exception_message - # @param [Boolean] response_body_present - # @param [Hash] response_body - # @param [Integer] response_status_code - def idv_arcgis_request_failure( - exception_class:, - exception_message:, - response_body_present:, - response_body:, - response_status_code:, - **extra - ) - track_event( - 'Request ArcGIS Address Candidates: request failed', - exception_class: exception_class, - exception_message: exception_message, - response_body_present: response_body_present, - response_body: response_body, - response_status_code: response_status_code, - **extra, - ) + # tracks if the session is kept alive + def session_kept_alive + track_event('Session Kept Alive') end - # Tracks whether the user's device appears to be mobile device with a camera attached. - # @param [Boolean] is_camera_capable_mobile Whether we think the device _could_ have a camera. - # @param [Boolean,nil] camera_present Whether the user's device _actually_ has a camera available. - # @param [Integer,nil] grace_time Extra time allowed for browser to report camera availability. - # @param [Integer,nil] duration Time taken for browser to report camera availability. - def idv_mobile_device_and_camera_check( - is_camera_capable_mobile:, - camera_present: nil, - grace_time: nil, - duration: nil, - **extra - ) + # tracks if the session timed out + def session_timed_out + track_event('Session Timed Out') + end + + # tracks when a user's session is timed out + def session_total_duration_timeout + track_event('User Maximum Session Length Exceeded') + end + + # Tracks if a user clicks the "Show Password button" + # @param [String] path URL path where the click occurred + def show_password_button_clicked(path:, **extra) + track_event('Show Password Button Clicked', path: path, **extra) + end + + # @param [String] flash + # @param [String] stored_location + # @param [String] sign_in_a_b_test_bucket + # tracks when a user visits the sign in page + def sign_in_page_visit(flash:, stored_location:, sign_in_a_b_test_bucket:, **extra) track_event( - 'IdV: Mobile device and camera check', - is_camera_capable_mobile: is_camera_capable_mobile, - camera_present: camera_present, - grace_time: grace_time, - duration: duration, + 'Sign in page visited', + flash: flash, + stored_location: stored_location, + sign_in_a_b_test_bucket:, **extra, ) end - # Tracks when the user visits one of the the session error pages. - # @param [String] type - # @param [Integer,nil] attempts_remaining - def idv_session_error_visited( - type:, - attempts_remaining: nil, + # @param [Boolean] success + # @param [Boolean] new_user + # @param [Boolean] has_other_auth_methods + # @param [Integer] phone_configuration_id + # tracks when a user opts into SMS + def sms_opt_in_submitted( + success:, + new_user:, + has_other_auth_methods:, + phone_configuration_id:, **extra ) track_event( - 'IdV: session error visited', - type: type, - attempts_remaining: attempts_remaining, + 'SMS Opt-In: Submitted', + success: success, + new_user: new_user, + has_other_auth_methods: has_other_auth_methods, + phone_configuration_id: phone_configuration_id, **extra, ) end - # Tracks if request to get USPS in-person proofing locations fails - # @param [String] exception_class - # @param [String] exception_message - # @param [Boolean] response_body_present - # @param [Hash] response_body - # @param [Integer] response_status_code - def idv_in_person_locations_request_failure( - exception_class:, - exception_message:, - response_body_present:, - response_body:, - response_status_code:, + # @param [Boolean] new_user + # @param [Boolean] has_other_auth_methods + # @param [Integer] phone_configuration_id + # tracks when a user visits the sms opt in page + def sms_opt_in_visit( + new_user:, + has_other_auth_methods:, + phone_configuration_id:, **extra ) track_event( - 'Request USPS IPP locations: request failed', - exception_class: exception_class, - exception_message: exception_message, - response_body_present: response_body_present, - response_body: response_body, - response_status_code: response_status_code, + 'SMS Opt-In: Visited', + new_user: new_user, + has_other_auth_methods: has_other_auth_methods, + phone_configuration_id: phone_configuration_id, **extra, ) end - # Tracks when USPS in-person proofing enrollment is created - # @param [String] enrollment_code - # @param [Integer] enrollment_id - # @param [String] service_provider - def usps_ippaas_enrollment_created( - enrollment_code:, - enrollment_id:, - service_provider:, - **extra - ) - track_event( - 'USPS IPPaaS enrollment created', - enrollment_code: enrollment_code, - enrollment_id: enrollment_id, - service_provider: service_provider, + # Tracks when a user is bounced back from the service provider due to an integration issue. + def sp_handoff_bounced_detected + track_event('SP handoff bounced detected') + end + + # Tracks when a user visits the bounced page. + def sp_handoff_bounced_visit + track_event('SP handoff bounced visited') + end + + # Tracks when a user visits the "This agency no longer uses Login.gov" page. + def sp_inactive_visit + track_event('SP inactive visited') + end + + # Tracks when a user is redirected back to the service provider + # @param [Integer] ial + # @param [Integer] billed_ial + def sp_redirect_initiated(ial:, billed_ial:, **extra) + track_event( + 'SP redirect initiated', + ial: ial, + billed_ial: billed_ial, **extra, ) end - # Tracks if USPS in-person proofing enrollment request fails - # @param [String] context - # @param [String] reason - # @param [Integer] enrollment_id - # @param [String] exception_class - # @param [String] exception_message - def idv_in_person_usps_request_enroll_exception( + # Tracks when service provider consent is revoked + # @param [String] issuer issuer of the service provider consent to be revoked + def sp_revoke_consent_revoked(issuer:, **extra) + track_event( + 'SP Revoke Consent: Revoked', + issuer: issuer, + **extra, + ) + end + + # Tracks when the page to revoke consent (unlink from) a service provider visited + # @param [String] issuer which issuer + def sp_revoke_consent_visited(issuer:, **extra) + track_event( + 'SP Revoke Consent: Visited', + issuer: issuer, + **extra, + ) + end + + # @param [String] area_code + # @param [String] country_code + # @param [String] phone_fingerprint the hmac fingerprint of the phone number formatted as e164 + # @param [String] context the context of the OTP, either "authentication" for confirmed phones + # or "confirmation" for unconfirmed + # @param ["sms","voice"] otp_delivery_preference the channel used to send the message + # @param [Boolean] resend + # @param [Hash] telephony_response + # @param [:test, :pinpoint] adapter which adapter the OTP was delivered with + # @param [Boolean] success + # A phone one-time password send was attempted + def telephony_otp_sent( + area_code:, + country_code:, + phone_fingerprint:, context:, - reason:, - enrollment_id:, - exception_class:, - exception_message:, + otp_delivery_preference:, + resend:, + telephony_response:, + adapter:, + success:, **extra ) track_event( - 'USPS IPPaaS enrollment failed', - context: context, - enrollment_id: enrollment_id, - exception_class: exception_class, - exception_message: exception_message, - reason: reason, + 'Telephony: OTP sent', + { + area_code: area_code, + country_code: country_code, + phone_fingerprint: phone_fingerprint, + context: context, + otp_delivery_preference: otp_delivery_preference, + resend: resend, + telephony_response: telephony_response, + adapter: adapter, + success: success, + **extra, + }, + ) + end + + # Tracks when a user triggered a rate limit throttle + # @param [String] throttle_type + def throttler_rate_limit_triggered(throttle_type:, **extra) + track_event( + 'Throttler Rate Limit Triggered', + throttle_type: throttle_type, **extra, ) end - # GetUspsProofingResultsJob is beginning. Includes some metadata about what the job will do - # @param [Integer] enrollments_count number of enrollments eligible for status check - # @param [Integer] reprocess_delay_minutes minimum delay since last status check - def idv_in_person_usps_proofing_results_job_started( - enrollments_count:, - reprocess_delay_minutes:, + # Tracks when a user visits TOTP device setup + # @param [Boolean] user_signed_up + # @param [Boolean] totp_secret_present + # @param [Integer] enabled_mfa_methods_count + def totp_setup_visit( + user_signed_up:, + totp_secret_present:, + enabled_mfa_methods_count:, **extra ) track_event( - 'GetUspsProofingResultsJob: Job started', - enrollments_count: enrollments_count, - reprocess_delay_minutes: reprocess_delay_minutes, + 'TOTP Setup Visited', + user_signed_up: user_signed_up, + totp_secret_present: totp_secret_present, + enabled_mfa_methods_count: enabled_mfa_methods_count, **extra, ) end - # GetUspsProofingResultsJob has completed. Includes counts of various outcomes encountered - # @param [Float] duration_seconds number of minutes the job was running - # @param [Integer] enrollments_checked number of enrollments eligible for status check - # @param [Integer] enrollments_errored number of enrollments for which we encountered an error - # @param [Integer] enrollments_expired number of enrollments which expired - # @param [Integer] enrollments_failed number of enrollments which failed identity proofing - # @param [Integer] enrollments_in_progress number of enrollments which did not have any change - # @param [Integer] enrollments_passed number of enrollments which passed identity proofing - def idv_in_person_usps_proofing_results_job_completed( - duration_seconds:, - enrollments_checked:, - enrollments_errored:, - enrollments_expired:, - enrollments_failed:, - enrollments_in_progress:, - enrollments_passed:, + # Tracks when a user disabled a TOTP device + def totp_user_disabled + track_event('TOTP: User Disabled') + end + + # @param [String] controller + # @param [String] referer + # @param [Boolean] user_signed_in + # Redirect was almost sent to an invalid external host unexpectedly + def unsafe_redirect_error( + controller:, + referer:, + user_signed_in: nil, **extra ) track_event( - 'GetUspsProofingResultsJob: Job completed', - duration_seconds: duration_seconds, - enrollments_checked: enrollments_checked, - enrollments_errored: enrollments_errored, - enrollments_expired: enrollments_expired, - enrollments_failed: enrollments_failed, - enrollments_in_progress: enrollments_in_progress, - enrollments_passed: enrollments_passed, + 'Unsafe Redirect', + controller: controller, + referer: referer, + user_signed_in: user_signed_in, **extra, ) end - # Tracks exceptions that are raised when running GetUspsProofingResultsJob - # @param [String] reason why was the exception raised? - # @param [String] enrollment_id - # @param [String] exception_class - # @param [String] exception_message - # @param [String] enrollment_code - # @param [Float] minutes_since_established - # @param [Float] minutes_since_last_status_check - # @param [Float] minutes_since_last_status_update - # @param [Float] minutes_to_completion - # @param [Boolean] fraud_suspected - # @param [String] primary_id_type - # @param [String] secondary_id_type - # @param [String] failure_reason - # @param [String] transaction_end_date_time - # @param [String] transaction_start_date_time - # @param [String] status - # @param [String] assurance_level - # @param [String] proofing_post_office - # @param [String] proofing_city - # @param [String] proofing_state - # @param [String] scan_count - # @param [String] response_message - # @param [Integer] response_status_code - def idv_in_person_usps_proofing_results_job_exception( - reason:, - enrollment_id:, - minutes_since_established:, - exception_class: nil, - exception_message: nil, - enrollment_code: nil, - minutes_since_last_status_check: nil, - minutes_since_last_status_update: nil, - minutes_to_completion: nil, - fraud_suspected: nil, - primary_id_type: nil, - secondary_id_type: nil, - failure_reason: nil, - transaction_end_date_time: nil, - transaction_start_date_time: nil, - status: nil, - assurance_level: nil, - proofing_post_office: nil, - proofing_city: nil, - proofing_state: nil, - scan_count: nil, - response_message: nil, - response_status_code: nil, - **extra - ) + # User has attempted to access an action that requires re-authenticating + # @param [String] auth_method + # @param [String] authenticated_at + def user_2fa_reauthentication_required(auth_method:, authenticated_at:, **extra) track_event( - 'GetUspsProofingResultsJob: Exception raised', - reason: reason, - enrollment_id: enrollment_id, - exception_class: exception_class, - exception_message: exception_message, - enrollment_code: enrollment_code, - minutes_since_established: minutes_since_established, - minutes_since_last_status_check: minutes_since_last_status_check, - minutes_since_last_status_update: minutes_since_last_status_update, - minutes_to_completion: minutes_to_completion, - fraud_suspected: fraud_suspected, - primary_id_type: primary_id_type, - secondary_id_type: secondary_id_type, - failure_reason: failure_reason, - transaction_end_date_time: transaction_end_date_time, - transaction_start_date_time: transaction_start_date_time, - status: status, - assurance_level: assurance_level, - proofing_post_office: proofing_post_office, - proofing_city: proofing_city, - proofing_state: proofing_state, - scan_count: scan_count, - response_message: response_message, - response_status_code: response_status_code, + 'User 2FA Reauthentication Required', + auth_method: auth_method, + authenticated_at: authenticated_at, **extra, ) end - # Tracks deadline email initiated during GetUspsProofingResultsJob - # @param [String] enrollment_id - def idv_in_person_usps_proofing_results_job_deadline_passed_email_initiated( - enrollment_id:, - **extra - ) + # User has been marked as authenticated + # @param [String] authentication_type + def user_marked_authed(authentication_type:, **extra) track_event( - 'GetUspsProofingResultsJob: deadline passed email initiated', - enrollment_id: enrollment_id, + 'User marked authenticated', + authentication_type: authentication_type, **extra, ) end - # Tracks exceptions that are raised when initiating deadline email in GetUspsProofingResultsJob - # @param [String] enrollment_id - # @param [String] exception_class - # @param [String] exception_message - def idv_in_person_usps_proofing_results_job_deadline_passed_email_exception( - enrollment_id:, - exception_class: nil, - exception_message: nil, - **extra - ) + # @param [Boolean] success + # @param [Hash] errors + # @param [String] sign_up_mfa_priority_bucket + # Tracks when the the user has selected and submitted additional MFA methods on user registration + def user_registration_2fa_additional_setup(success:, + sign_up_mfa_priority_bucket:, + errors: nil, + **extra) track_event( - 'GetUspsProofingResultsJob: Exception raised when attempting to send deadline passed email', - enrollment_id: enrollment_id, - exception_class: exception_class, - exception_message: exception_message, + 'User Registration: Additional 2FA Setup', + { + success: success, + sign_up_mfa_priority_bucket: sign_up_mfa_priority_bucket, + errors: errors, + **extra, + }.compact, + ) + end + + # Tracks when user visits additional MFA selection page + # @param [String] sign_up_mfa_priority_bucket + def user_registration_2fa_additional_setup_visit(sign_up_mfa_priority_bucket:, **extra) + track_event( + 'User Registration: Additional 2FA Setup visited', + sign_up_mfa_priority_bucket: sign_up_mfa_priority_bucket, **extra, ) end - # Tracks exceptions that are raised when running InPerson::EmailReminderJob - # @param [String] enrollment_id - # @param [String] exception_class - # @param [String] exception_message - def idv_in_person_email_reminder_job_exception( - enrollment_id:, - exception_class: nil, - exception_message: nil, + # @param [Boolean] success + # @param [Hash] errors + # @param [Integer] enabled_mfa_methods_count + # @param [Integer] selected_mfa_count + # @param ['voice', 'auth_app'] selection + # @param [String] sign_up_mfa_priority_bucket + # Tracks when the the user has selected and submitted MFA auth methods on user registration + def user_registration_2fa_setup( + success:, + sign_up_mfa_priority_bucket:, + errors: nil, + selected_mfa_count: nil, + enabled_mfa_methods_count: nil, + selection: nil, **extra ) track_event( - 'InPerson::EmailReminderJob: Exception raised when attempting to send reminder email', - enrollment_id: enrollment_id, - exception_class: exception_class, - exception_message: exception_message, + 'User Registration: 2FA Setup', + { + success: success, + errors: errors, + selected_mfa_count: selected_mfa_count, + enabled_mfa_methods_count: enabled_mfa_methods_count, + sign_up_mfa_priority_bucket: sign_up_mfa_priority_bucket, + selection: selection, + **extra, + }.compact, + ) + end + + # Tracks when user visits MFA selection page + # @param [String] sign_up_mfa_priority_bucket + def user_registration_2fa_setup_visit(sign_up_mfa_priority_bucket:, **extra) + track_event( + 'User Registration: 2FA Setup visited', + sign_up_mfa_priority_bucket: sign_up_mfa_priority_bucket, **extra, ) end - # Tracks individual enrollments that are updated during GetUspsProofingResultsJob - # @param [String] enrollment_code - # @param [String] enrollment_id - # @param [Float] minutes_since_established - # @param [Boolean] fraud_suspected - # @param [Boolean] passed did this enrollment pass or fail? - # @param [String] reason why did this enrollment pass or fail? - def idv_in_person_usps_proofing_results_job_enrollment_updated( - enrollment_code:, - enrollment_id:, - minutes_since_established:, - fraud_suspected:, - passed:, - reason:, + # User registration has been handed off to agency page + # @param [Boolean] ial2 + # @param [Integer] ialmax + # @param [String] service_provider_name + # @param [String] page_occurence + # @param [String] needs_completion_screen_reason + # @param [Array] sp_request_requested_attributes + # @param [Array] sp_session_requested_attributes + def user_registration_agency_handoff_page_visit( + ial2:, + service_provider_name:, + page_occurence:, + needs_completion_screen_reason:, + sp_session_requested_attributes:, + sp_request_requested_attributes: nil, + ialmax: nil, + **extra + ) + track_event( + 'User registration: agency handoff visited', + ial2: ial2, + ialmax: ialmax, + service_provider_name: service_provider_name, + page_occurence: page_occurence, + needs_completion_screen_reason: needs_completion_screen_reason, + sp_request_requested_attributes: sp_request_requested_attributes, + sp_session_requested_attributes: sp_session_requested_attributes, + **extra, + ) + end + + # Tracks when user cancels registration + # @param [String] request_came_from the controller/action the request came from + def user_registration_cancellation(request_came_from:, **extra) + track_event( + 'User registration: cancellation visited', + request_came_from: request_came_from, + **extra, + ) + end + + # Tracks when user completes registration + # @param [Boolean] ial2 + # @param [Boolean] ialmax + # @param [String] service_provider_name + # @param [String] page_occurence + # @param [String] needs_completion_screen_reason + # @param [String] sign_in_a_b_test_bucket + # @param [Array] sp_request_requested_attributes + # @param [Array] sp_session_requested_attributes + def user_registration_complete( + ial2:, + service_provider_name:, + page_occurence:, + needs_completion_screen_reason:, + sign_in_a_b_test_bucket:, + sp_session_requested_attributes:, + sp_request_requested_attributes: nil, + ialmax: nil, **extra ) track_event( - 'GetUspsProofingResultsJob: Enrollment status updated', - enrollment_code: enrollment_code, - enrollment_id: enrollment_id, - minutes_since_established: minutes_since_established, - fraud_suspected: fraud_suspected, - passed: passed, - reason: reason, + 'User registration: complete', + ial2: ial2, + ialmax: ialmax, + service_provider_name: service_provider_name, + page_occurence: page_occurence, + needs_completion_screen_reason: needs_completion_screen_reason, + sign_in_a_b_test_bucket:, + sp_request_requested_attributes: sp_request_requested_attributes, + sp_session_requested_attributes: sp_session_requested_attributes, **extra, ) end - # Tracks emails that are initiated during GetUspsProofingResultsJob - # @param [String] email_type success, failed or failed fraud - def idv_in_person_usps_proofing_results_job_email_initiated( - email_type:, + # Tracks when user submits registration email + # @param [Boolean] success + # @param [Boolean] throttled + # @param [Hash] errors + # @param [Hash] error_details + # @param [String] user_id + # @param [String] domain_name + def user_registration_email( + success:, + throttled:, + errors:, + error_details: nil, + user_id: nil, + domain_name: nil, **extra ) track_event( - 'GetUspsProofingResultsJob: Success or failure email initiated', - email_type: email_type, - **extra, + 'User Registration: Email Submitted', + { + success: success, + throttled: throttled, + errors: errors, + error_details: error_details, + user_id: user_id, + domain_name: domain_name, + **extra, + }.compact, ) end - # Tracks emails that are initiated during InPerson::EmailReminderJob - # @param [String] email_type early or late - # @param [String] enrollment_id - def idv_in_person_email_reminder_job_email_initiated( - email_type:, - enrollment_id:, + # Tracks when user confirms registration email + # @param [Boolean] success + # @param [Hash] errors + # @param [Hash] error_details + # @param [String] user_id + def user_registration_email_confirmation( + success:, + errors:, + error_details: nil, + user_id: nil, **extra ) track_event( - 'InPerson::EmailReminderJob: Reminder email initiated', - email_type: email_type, - enrollment_id: enrollment_id, + 'User Registration: Email Confirmation', + success: success, + errors: errors, + error_details: error_details, + user_id: user_id, **extra, ) end - # Tracks incomplete enrollments checked via the USPS API - # @param [String] enrollment_code - # @param [String] enrollment_id - # @param [Float] minutes_since_established - # @param [String] response_message - def idv_in_person_usps_proofing_results_job_enrollment_incomplete( - enrollment_code:, - enrollment_id:, - minutes_since_established:, - response_message:, - **extra - ) + # Tracks when user visits enter email page + # @param [String] sign_in_a_b_test_bucket + # @param [Boolean] from_sign_in + def user_registration_enter_email_visit(sign_in_a_b_test_bucket:, from_sign_in:, **extra) track_event( - 'GetUspsProofingResultsJob: Enrollment incomplete', - enrollment_code: enrollment_code, - enrollment_id: enrollment_id, - minutes_since_established: minutes_since_established, - response_message: response_message, + 'User Registration: enter email visited', + sign_in_a_b_test_bucket:, + from_sign_in:, **extra, ) end - # Tracks unexpected responses from the USPS API - # @param [String] enrollment_code - # @param [String] enrollment_id - # @param [Float] minutes_since_established - # @param [String] response_message - # @param [String] reason why was this error unexpected? - def idv_in_person_usps_proofing_results_job_unexpected_response( - enrollment_code:, - enrollment_id:, - minutes_since_established:, - response_message:, - reason:, + # @param [Boolean] success + # @param [Hash] mfa_method_counts + # @param [Integer] enabled_mfa_methods_count + # @param [Hash] pii_like_keypaths + # Tracks when a user has completed MFA setup + def user_registration_mfa_setup_complete( + success:, + mfa_method_counts:, + enabled_mfa_methods_count:, + pii_like_keypaths:, **extra ) track_event( - 'GetUspsProofingResultsJob: Unexpected response received', - enrollment_code: enrollment_code, - enrollment_id: enrollment_id, - minutes_since_established: minutes_since_established, - response_message: response_message, - reason: reason, - **extra, + 'User Registration: MFA Setup Complete', + { + success: success, + mfa_method_counts: mfa_method_counts, + enabled_mfa_methods_count: enabled_mfa_methods_count, + pii_like_keypaths: pii_like_keypaths, + **extra, + }.compact, ) end - # Tracks users visiting the recovery options page - def account_reset_recovery_options_visit - track_event('Account Reset: Recovery Options Visited') + # @param [Integer] enabled_mfa_methods_count + # Tracks when user visits the phone setup step during registration + def user_registration_phone_setup_visit(enabled_mfa_methods_count:, **extra) + track_event( + 'User Registration: phone setup visited', + enabled_mfa_methods_count: enabled_mfa_methods_count, + **extra, + ) end - # Tracks users going back or cancelling acoount recovery - def cancel_account_reset_recovery - track_event('Account Reset: Cancel Account Recovery Options') + # Tracks when user's piv cac is disabled + def user_registration_piv_cac_disabled + track_event('User Registration: piv cac disabled') end - # @identity.idp.previous_event_name IdV: Verify setup errors visited - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # Tracks when the user reaches the verify please call page after failing proofing - def idv_please_call_visited(proofing_components: nil, **extra) + # Tracks when user's piv cac setup + def user_registration_piv_cac_setup_visit(**extra) track_event( - 'IdV: Verify please call visited', - proofing_components: proofing_components, + 'User Registration: piv cac setup visited', **extra, ) end - # Tracks when user reaches verify errors due to being rejected due to fraud - def idv_not_verified_visited - track_event('IdV: Not verified visited') + # Tracks when user skips Suggest Another MFA Page + def user_registration_suggest_another_mfa_notice_skipped + track_event('User Registration: Suggest Another MFA Notice Skipped') end - # @param [String] redirect_url URL user was directed to - # @param [String, nil] step which step - # @param [String, nil] location which part of a step, if applicable - # @param ["idv", String, nil] flow which flow - # User was redirected to the login.gov contact page - def contact_redirect(redirect_url:, step: nil, location: nil, flow: nil, **extra) + # Tracks when user visits Suggest Another MFA Page + def user_registration_suggest_another_mfa_notice_visited + track_event('User Registration: Suggest Another MFA Notice visited') + end + + # @param [String] mfa_method + # Tracks when the the user fully registered by submitting their first MFA method into the system + def user_registration_user_fully_registered( + mfa_method:, + **extra + ) track_event( - 'Contact Page Redirect', - redirect_url: redirect_url, - step: step, - location: location, - flow: flow, - **extra, + 'User Registration: User Fully Registered', + { + mfa_method: mfa_method, + **extra, + }.compact, ) end - # @param [String] redirect_url URL user was directed to - # @param [String, nil] step which step - # @param [String, nil] location which part of a step, if applicable - # @param ["idv", String, nil] flow which flow - # User was redirected to the login.gov policy page - def policy_redirect(redirect_url:, step: nil, location: nil, flow: nil, **extra) + # Tracks when USPS in-person proofing enrollment is created + # @param [String] enrollment_code + # @param [Integer] enrollment_id + # @param [String] service_provider + def usps_ippaas_enrollment_created( + enrollment_code:, + enrollment_id:, + service_provider:, + **extra + ) track_event( - 'Policy Page Redirect', - redirect_url: redirect_url, - step: step, - location: location, - flow: flow, + 'USPS IPPaaS enrollment created', + enrollment_code: enrollment_code, + enrollment_id: enrollment_id, + service_provider: service_provider, **extra, ) end - # Tracks if a user clicks the "Show Password button" - # @param [String] path URL path where the click occurred - def show_password_button_clicked(path:, **extra) - track_event('Show Password Button Clicked', path: path, **extra) + # @param [Hash] vendor_status + # @param [String,nil] redirect_from + # Tracks when vendor has outage + def vendor_outage( + vendor_status:, + redirect_from: nil, + **extra + ) + track_event( + 'Vendor Outage', + redirect_from: redirect_from, + vendor_status: vendor_status, + **extra, + ) end - # Tracks if a user clicks the 'acknowledge' checkbox during personal - # key creation - # @param [Idv::ProofingComponentsLogging] proofing_components User's current proofing components - # @param [boolean] checked whether the user checked or un-checked - # the box with this click - def idv_personal_key_acknowledgment_toggled(checked:, proofing_components:, **extra) + # @param [Boolean] success + # @param [Integer] mfa_method_counts + # Tracks when WebAuthn is deleted + def webauthn_deleted(success:, mfa_method_counts:, pii_like_keypaths:, **extra) track_event( - 'IdV: personal key acknowledgment toggled', - checked: checked, - proofing_components: proofing_components, + 'WebAuthn Deleted', + success: success, + mfa_method_counts: mfa_method_counts, + pii_like_keypaths: pii_like_keypaths, **extra, ) end - # Logs after an email is sent - # @param [String] action type of email being sent - # @param [String, nil] ses_message_id AWS SES Message ID - def email_sent(action:, ses_message_id:, **extra) + # @param [Hash] platform_authenticator + # @param [Hash] errors + # @param [Integer] enabled_mfa_methods_count + # @param [Boolean] success + # Tracks when WebAuthn setup is visited + def webauthn_setup_visit(platform_authenticator:, errors:, enabled_mfa_methods_count:, success:, + **extra) track_event( - 'Email Sent', - action: action, - ses_message_id: ses_message_id, + 'WebAuthn Setup Visited', + platform_authenticator: platform_authenticator, + errors: errors, + enabled_mfa_methods_count: enabled_mfa_methods_count, + success: success, **extra, ) end From 0a8ab977e9c99ebc0c1bcd16390156f4f0ebfcd6 Mon Sep 17 00:00:00 2001 From: Matt Gardner Date: Thu, 1 Jun 2023 12:47:44 -0400 Subject: [PATCH 02/20] changelog: Internal, In-Person Proofing, Use Rails-like i18n wrapper instead (#9165) (#8522) --- app/javascript/packages/address-search/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/javascript/packages/address-search/index.tsx b/app/javascript/packages/address-search/index.tsx index 63bc2527b11..960f02d8613 100644 --- a/app/javascript/packages/address-search/index.tsx +++ b/app/javascript/packages/address-search/index.tsx @@ -1,6 +1,6 @@ import { TextInput } from '@18f/identity-components'; import { useState, useRef, useEffect, useCallback } from 'react'; -import { useI18n } from '@18f/identity-react-i18n'; +import { t } from '@18f/identity-i18n'; import { request } from '@18f/identity-request'; import ValidatedField from '@18f/identity-validated-field/validated-field'; import SpinnerButton, { SpinnerButtonRefHandle } from '@18f/identity-spinner-button/spinner-button'; @@ -104,7 +104,6 @@ function useUspsLocations() { // raw text input that is set when user clicks search const [addressQuery, setAddressQuery] = useState(''); const validatedFieldRef = useRef(null); - const { t } = useI18n(); const handleAddressSearch = useCallback((event, unvalidatedAddressInput) => { event.preventDefault(); validatedFieldRef.current?.setCustomValidity(''); @@ -179,7 +178,6 @@ function AddressSearch({ onError = () => undefined, disabled = false, }: AddressSearchProps) { - const { t } = useI18n(); const spinnerButtonRef = useRef(null); const [textInput, setTextInput] = useState(''); const { From 407b4fad99be5844b098091ca134c035063bd744 Mon Sep 17 00:00:00 2001 From: Brittany Greaner <35475380+night-jellyfish@users.noreply.github.com> Date: Thu, 1 Jun 2023 11:56:23 -0500 Subject: [PATCH 03/20] Upgrade to postgres 14 (#8518) changelog: Internal, Application dependencies, Upgrade postgres to version 14 In #8475, we chose to keep Postgres at version 13 so that it could [match production](https://github.com/18F/identity-devops/blob/33ba02736b37f3a79c50479892a03c6f3e920041/terraform/app/rds-variables.tf#L82). However, when trying to continue on the `postgis` work, I found that when installing `postgis`, `brew` installs 14 anyway, as a dependency of `postgis`. There doesn't seem to be an easy way to tell Brew not to install 14, or postgis to use 13 instead. This becomes an issue then when the app is using 13, but `postgis` is using 14. I thought it'd be better to change this now than wait for the postgis work to be ready, as it would potentially stop folks from installing an extra version of postgres. There may be another workaround, but after reading [a homebrew issue about versioning issues with postgres and postgis](https://github.com/Homebrew/homebrew-core/issues/86709), which included suggestions like [making your own copy of `postgis` and changing its dependencies](https://github.com/Homebrew/homebrew-core/issues/86709#issuecomment-936462624), it seemed like upgrading `postgres` was a better solution. While it will mean dev is on a different version than prod, most folks have been using 14 or even 15, so we don't expect issues to arise. --- Brewfile | 2 +- bin/setup | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Brewfile b/Brewfile index fae834dac18..ac26eeacf37 100644 --- a/Brewfile +++ b/Brewfile @@ -1,4 +1,4 @@ -brew 'postgresql@13' +brew 'postgresql@14' brew 'redis' brew 'node@16' brew 'yarn' diff --git a/bin/setup b/bin/setup index e8431e847b2..4a92358cb1d 100755 --- a/bin/setup +++ b/bin/setup @@ -62,7 +62,7 @@ Dir.chdir APP_ROOT do puts "\n== Starting services ==" run "brew services start redis" if brew_installed - run "brew services start postgresql@13" if brew_installed + run "brew services start postgresql@14" if brew_installed puts "\n== Preparing database ==" run 'make clobber_db' From 1e7362d13ebbe8ebdf727547c3ac0efa8d1c6e16 Mon Sep 17 00:00:00 2001 From: dawei-nava <130466753+dawei-nava@users.noreply.github.com> Date: Thu, 1 Jun 2023 13:52:18 -0400 Subject: [PATCH 04/20] LG-9825: Add back-end validation for the DOB on the state id form (#8501) * LG-9825: validate dob for minimum age. * LG-9825: validate dob for minimum age. and test cases. changelog: Internal, In-Person Proofing, Add DOB validation for minimal age. * LG-9825: no need to convert to string. * LG-9825: show backend validation error in UI. * LG-9825: linter issue. * LG-9825: linter issue. * LG-9825: view component test update. * LG-9825: update error message. * LG-9825: update test for error message. * LG-9825: boolean method. * LG-9825: Address some commments. Check cleaned error emptiness for FormResponse success flag. * LG-9825: Hmm, looks like tests expect this real validation result. * LG-9825: test. * LG-9825: use rails 7 Comparison validator. * LG-9825: rarely when it cannot be parsed to Date. * LG-9825: refactor to extends rails7 ComparisonValidator. * LG-9825: use duck typing syntax. * LG-9825: clean up. * LG-9825: address comment. * LG-9825: address comment. * LG-9825: address form related fix. * LG-9825: doc update. * LG-9825: test updates. * LG-9825: context name etc in tests. * LG-9825: context name etc in tests. --- .../memorable_date_component.html.erb | 16 ++- app/components/memorable_date_component.rb | 8 ++ app/forms/idv/in_person/address_form.rb | 3 +- app/forms/idv/state_id_form.rb | 4 +- .../usps_in_person_proofing/date_validator.rb | 37 ++++++ app/validators/idv/form_state_id_validator.rb | 12 ++ .../memorable_date_component_spec.rb | 13 +++ spec/forms/idv/in_person/address_form_spec.rb | 103 +++++++++++++++++ spec/forms/idv/state_id_form_spec.rb | 108 ++++++++++++++++++ 9 files changed, 297 insertions(+), 7 deletions(-) create mode 100644 app/services/usps_in_person_proofing/date_validator.rb create mode 100644 spec/forms/idv/in_person/address_form_spec.rb create mode 100644 spec/forms/idv/state_id_form_spec.rb diff --git a/app/components/memorable_date_component.html.erb b/app/components/memorable_date_component.html.erb index ceb2eb1fe7f..69c1944c253 100644 --- a/app/components/memorable_date_component.html.erb +++ b/app/components/memorable_date_component.html.erb @@ -35,11 +35,12 @@ minLength: 1, maxLength: 2, aria: { - invalid: false, + invalid: has_errors?, labelledby: [ "memorable-date-month-label-#{unique_id}", "memorable-date-month-hint-#{unique_id}", ], + describedby: ["validated-field-error-#{unique_id}"], }, value: month, }, @@ -71,11 +72,13 @@ minLength: 1, maxLength: 2, aria: { - invalid: false, + invalid: has_errors?, labelledby: [ "memorable-date-day-label-#{unique_id}", "memorable-date-day-hint-#{unique_id}", ], + describedby: ["validated-field-error-#{unique_id}"], + }, value: day, }, @@ -107,11 +110,12 @@ minLength: 4, maxLength: 4, aria: { - invalid: false, + invalid: has_errors?, labelledby: [ "memorable-date-year-label-#{unique_id}", "memorable-date-year-hint-#{unique_id}", ], + describedby: ["validated-field-error-#{unique_id}"], }, value: year, }, @@ -125,4 +129,8 @@ <% end -%> - +
+ <% if has_errors? %> + <%= error_msg %> + <% end %> +
diff --git a/app/components/memorable_date_component.rb b/app/components/memorable_date_component.rb index 1e096caa389..bb33200139c 100644 --- a/app/components/memorable_date_component.rb +++ b/app/components/memorable_date_component.rb @@ -122,6 +122,14 @@ def i18n_long_format(date) end end + def has_errors? + form.object.respond_to?(:errors) && form.object.errors.key?(name) + end + + def error_msg + form.object.errors[name]&.first + end + # Configure default generic error messages for component, # then integrate any overrides def generate_error_messages(label, min, max, override_error_messages) diff --git a/app/forms/idv/in_person/address_form.rb b/app/forms/idv/in_person/address_form.rb index d53513a6e43..43d5b422cca 100644 --- a/app/forms/idv/in_person/address_form.rb +++ b/app/forms/idv/in_person/address_form.rb @@ -19,13 +19,14 @@ def self.model_name def submit(params) consume_params(params) + validation_success = valid? cleaned_errors = errors.dup cleaned_errors.delete(:city, :nontransliterable_field) cleaned_errors.delete(:address1, :nontransliterable_field) cleaned_errors.delete(:address2, :nontransliterable_field) FormResponse.new( - success: valid?, + success: validation_success, errors: cleaned_errors, ) end diff --git a/app/forms/idv/state_id_form.rb b/app/forms/idv/state_id_form.rb index 388309d3d46..2225225344a 100644 --- a/app/forms/idv/state_id_form.rb +++ b/app/forms/idv/state_id_form.rb @@ -19,7 +19,7 @@ def initialize(pii) def submit(params) consume_params(params) - + validation_success = valid? cleaned_errors = errors.dup cleaned_errors.delete(:first_name, :nontransliterable_field) cleaned_errors.delete(:last_name, :nontransliterable_field) @@ -28,7 +28,7 @@ def submit(params) cleaned_errors.delete(:identity_doc_address2, :nontransliterable_field) FormResponse.new( - success: valid?, + success: validation_success, errors: cleaned_errors, ) end diff --git a/app/services/usps_in_person_proofing/date_validator.rb b/app/services/usps_in_person_proofing/date_validator.rb new file mode 100644 index 00000000000..02b66c6df4d --- /dev/null +++ b/app/services/usps_in_person_proofing/date_validator.rb @@ -0,0 +1,37 @@ +module UspsInPersonProofing + # Validator that can be attached to a form or other model + # to verify that specific supported fields are comparable to other dates. + # Since it's a subclass of ComparisonValidator, it share the same options. + # == Example + # + # validates_with UspsInPersonProofing::DateValidator, + # attributes: [:dob], + # message: "error", + # less_than_or_equal_to: ->(_rec) { + # Time.zone.today - IdentityConfig.store.idv_min_age_years.years + # } + # .... + # + class DateValidator < ActiveModel::Validations::ComparisonValidator + private + + def prepare_value_for_validation(value, _record, _attr_name) + val_to_date(value) + rescue + nil + end + + # @param [String,Date,#to_hash] param + # @return [Date] + # It's caller's responsibility to ensure the param contains required entries + def val_to_date(param) + case param + when String, Date + DateParser.parse_legacy(param) + else + h = param.to_hash.with_indifferent_access + Date.new(h[:year].to_i, h[:month].to_i, h[:day].to_i) + end + end + end +end diff --git a/app/validators/idv/form_state_id_validator.rb b/app/validators/idv/form_state_id_validator.rb index 89c4dc03d18..6e409bd9916 100644 --- a/app/validators/idv/form_state_id_validator.rb +++ b/app/validators/idv/form_state_id_validator.rb @@ -2,6 +2,7 @@ module Idv module FormStateIdValidator extend ActiveSupport::Concern + # rubocop:disable Metrics/BlockLength included do validates :first_name, :last_name, @@ -29,6 +30,17 @@ module FormStateIdValidator char_list: invalid_chars.join(', '), ) end + # rubocop:disable Layout/LineLength + validates_with UspsInPersonProofing::DateValidator, + attributes: [:dob], less_than_or_equal_to: ->(_rec) { + Time.zone.today - IdentityConfig.store.idv_min_age_years.years + }, + message: I18n.t( + 'in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.range_min_age', + app_name: APP_NAME, + ) + # rubocop:enable Layout/LineLength end + # rubocop:enable Metrics/BlockLength end end diff --git a/spec/components/memorable_date_component_spec.rb b/spec/components/memorable_date_component_spec.rb index 164ff54991d..02bb5e2f4dd 100644 --- a/spec/components/memorable_date_component_spec.rb +++ b/spec/components/memorable_date_component_spec.rb @@ -278,4 +278,17 @@ MemorableDateComponent.extract_date_param('abcd'), ).to be_nil end + + context 'backend validation error message' do + let(:backend_error) { 'backend error' } + it 'renders a visible error message element' do + allow(form_builder.object).to receive(:errors).and_return( + { + name => [backend_error], + }, + ) + expect(rendered).not_to have_css('.usa-error-message.display-none') + expect(rendered.css('.usa-error-message')).to have_text(backend_error) + end + end end diff --git a/spec/forms/idv/in_person/address_form_spec.rb b/spec/forms/idv/in_person/address_form_spec.rb new file mode 100644 index 00000000000..aef7424b8d7 --- /dev/null +++ b/spec/forms/idv/in_person/address_form_spec.rb @@ -0,0 +1,103 @@ +require 'rails_helper' + +describe Idv::InPerson::AddressForm do + let(:pii) { Idp::Constants::MOCK_IDV_APPLICANT_STATE_ID_ADDRESS } + context 'test validation for transliteration after form submission' do + let(:good_params) do + { + address1: Faker::Address.street_address, + address2: Faker::Address.secondary_address, + zipcode: Faker::Address.zip_code, + state: Faker::Address.state_abbr, + city: Faker::Address.city, + } + end + let(:invalid_char) { '$' } + let(:bad_params) do + { + address1: invalid_char + Faker::Address.street_address, + address2: invalid_char + Faker::Address.secondary_address + invalid_char, + zipcode: Faker::Address.zip_code, + state: Faker::Address.state_abbr, + city: invalid_char + Faker::Address.city, + } + end + context 'when usps_ipp_transliteration_enabled is false' do + let(:subject) { described_class.new(capture_secondary_id_enabled: true) } + before(:each) do + allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled).and_return(false) + end + it 'submit success with good params' do + good_params[:same_address_as_id] = true + result = subject.submit(good_params) + expect(subject.errors.empty?).to be(true) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be(true) + end + + it 'submit success with good params' do + good_params[:same_address_as_id] = false + result = subject.submit(good_params) + expect(subject.errors.empty?).to be(true) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be(true) + end + + it 'submit success with bad params' do + bad_params[:same_address_as_id] = false + result = subject.submit(bad_params) + expect(subject.errors.empty?).to be(true) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be(true) + end + end + context 'when usps_ipp_transliteration_enabled is enabled ' do + let(:subject) { described_class.new(capture_secondary_id_enabled: true) } + before(:each) do + allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled).and_return(true) + end + it 'submit success with good params' do + good_params[:same_address_as_id] = true + result = subject.submit(good_params) + expect(subject.errors.empty?).to be(true) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be(true) + end + it 'submit failure with bad params' do + bad_params[:same_address_as_id] = false + result = subject.submit(bad_params) + expect(subject.errors.empty?).to be(false) + expect(subject.errors.to_hash).to include(:address1, :address2, :city) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be(false) + expect(result.errors.empty?).to be(true) + end + end + context 'when usps_ipp_transliteration_enabled is enabled and validate on other field' do + before(:each) do + allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled).and_return(true) + end + context 'when capture_secondary_id_enabled is true' do + let(:subject) { described_class.new(capture_secondary_id_enabled: true) } + it 'submit with missing same_address_as_id should be successful' do + missing_required_params = good_params.except(:same_address_as_id) + result = subject.submit(missing_required_params) + expect(subject.errors.empty?).to be(true) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be(true) + end + end + context 'when capture_secondary_id_enabled is false' do + let(:subject) { described_class.new(capture_secondary_id_enabled: false) } + it 'submit with missing same_address_as_id will fail' do + missing_required_params = good_params.except(:same_address_as_id) + result = subject.submit(missing_required_params) + expect(subject.errors.empty?).to be(false) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to be(false) + expect(result.errors.keys).to include(:same_address_as_id) + end + end + end + end +end diff --git a/spec/forms/idv/state_id_form_spec.rb b/spec/forms/idv/state_id_form_spec.rb new file mode 100644 index 00000000000..30b8142ca27 --- /dev/null +++ b/spec/forms/idv/state_id_form_spec.rb @@ -0,0 +1,108 @@ +require 'rails_helper' + +describe Idv::StateIdForm do + let(:subject) { Idv::StateIdForm.new(pii) } + let(:valid_dob) do + valid_d = Time.zone.today - IdentityConfig.store.idv_min_age_years.years - 1.day + ActionController::Parameters.new( + { + year: valid_d.year, + month: valid_d.month, + day: valid_d.mday, + }, + ).permit(:year, :month, :day) + end + let(:too_young_dob) do + (Time.zone.today - IdentityConfig.store.idv_min_age_years.years + 1.day).to_s + end + let(:good_params) do + { + first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + dob: valid_dob, + identity_doc_address1: Faker::Address.street_address, + identity_doc_address2: Faker::Address.secondary_address, + identity_doc_zipcode: Faker::Address.zip_code, + identity_doc_address_state: Faker::Address.state_abbr, + same_address_as_id: 'true', + state_id_jurisdiction: 'AL', + state_id_number: Faker::IDNumber.valid, + } + end + let(:dob_min_age_name_error_params) do + { + first_name: Faker::Name.first_name + invalid_char, + last_name: Faker::Name.last_name, + dob: too_young_dob, + identity_doc_address1: Faker::Address.street_address, + identity_doc_address2: Faker::Address.secondary_address, + identity_doc_zipcode: Faker::Address.zip_code, + identity_doc_address_state: Faker::Address.state_abbr, + same_address_as_id: 'true', + state_id_jurisdiction: 'AL', + state_id_number: Faker::IDNumber.valid, + } + end + let(:invalid_char) { '1' } + let(:name_error_params) do + { + first_name: Faker::Name.first_name + invalid_char, + last_name: Faker::Name.last_name, + dob: valid_dob, + identity_doc_address1: Faker::Address.street_address, + identity_doc_address2: Faker::Address.secondary_address, + identity_doc_zipcode: Faker::Address.zip_code, + identity_doc_address_state: Faker::Address.state_abbr, + same_address_as_id: 'true', + state_id_jurisdiction: 'AL', + state_id_number: Faker::IDNumber.valid, + } + end + let(:pii) { nil } + describe '#submit' do + context 'when the form is valid' do + it 'returns a successful form response' do + result = subject.submit(good_params) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(true) + expect(result.errors).to be_empty + end + end + + context 'when there is an error with name' do + it 'returns a single name error when name is wrong' do + allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled).and_return(true) + result = subject.submit(name_error_params) + expect(subject.errors.empty?).to be(false) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(subject.errors[:first_name]).to eq [ + I18n.t( + 'in_person_proofing.form.state_id.errors.unsupported_chars', + char_list: [invalid_char].join(', '), + ), + ] + expect(result.errors.empty?).to be(true) + end + it 'returns both name and dob error when both fields are invalid' do + allow(IdentityConfig.store).to receive(:usps_ipp_transliteration_enabled).and_return(true) + result = subject.submit(dob_min_age_name_error_params) + expect(subject.errors.empty?).to be(false) + expect(result).to be_kind_of(FormResponse) + expect(result.success?).to eq(false) + expect(subject.errors[:first_name]).to eq [ + I18n.t( + 'in_person_proofing.form.state_id.errors.unsupported_chars', + char_list: [invalid_char].join(', '), + ), + ] + expect(subject.errors[:dob]).to eq [ + I18n.t( + 'in_person_proofing.form.state_id.memorable_date.errors.date_of_birth.range_min_age', + app_name: APP_NAME, + ), + ] + end + end + end +end From 27b91b74cf4e9ff3b582c62e2b16bea616c372fa Mon Sep 17 00:00:00 2001 From: Amir Reavis-Bey <1261794+amirbey@users.noreply.github.com> Date: Thu, 1 Jun 2023 15:17:03 -0400 Subject: [PATCH 05/20] Remove DocumentCaptureController direct usage of effective_user (#8505) * IdvSession to user current_user by default and effective user when current user is not present * changelog: Internal, Refactor, IdVSession to concern to use current_user by default * remove effective user since current user exists in controllers using StepUtilitiesConcern * comment usage of current_user in IDVSession concern * temp test to demonstrate DocumentCapture controller does not directly use effective_user * remove effective user from idv session concern * replace effective_user with idv_session_user in SessionErrorsController * controllers that include this concern all have current user and hybrid document capture does not include this concern * idv sessoion user needed incase user is coming from hybrid doc cap to another step like link_sent * add IdvSession to RateLimitConcern test controller --------- Co-authored-by: AmirReavis-Bey --- .../concerns/idv/step_utilities_concern.rb | 2 +- app/controllers/concerns/idv_session.rb | 21 ++++++++++++------- .../concerns/rate_limit_concern.rb | 5 ++--- .../idv/session_errors_controller.rb | 7 +++---- .../concerns/rate_limit_concern_spec.rb | 1 + .../idv/document_capture_controller_spec.rb | 7 +++++++ 6 files changed, 27 insertions(+), 16 deletions(-) diff --git a/app/controllers/concerns/idv/step_utilities_concern.rb b/app/controllers/concerns/idv/step_utilities_concern.rb index 3d50f23bf4a..51e4ac581e0 100644 --- a/app/controllers/concerns/idv/step_utilities_concern.rb +++ b/app/controllers/concerns/idv/step_utilities_concern.rb @@ -4,7 +4,7 @@ module StepUtilitiesConcern include AcuantConcern def irs_reproofing? - effective_user&.reproof_for_irs?( + current_user&.reproof_for_irs?( service_provider: current_sp, ).present? end diff --git a/app/controllers/concerns/idv_session.rb b/app/controllers/concerns/idv_session.rb index 46091d2720d..75f1d59df5c 100644 --- a/app/controllers/concerns/idv_session.rb +++ b/app/controllers/concerns/idv_session.rb @@ -1,16 +1,15 @@ module IdvSession extend ActiveSupport::Concern - include EffectiveUser included do - before_action :redirect_unless_effective_user + before_action :redirect_unless_idv_session_user before_action :redirect_if_sp_context_needed end def confirm_idv_needed - return if effective_user.active_profile.blank? || + return if idv_session_user.active_profile.blank? || decorated_session.requested_more_recent_verification? || - effective_user.reproof_for_irs?(service_provider: current_sp) + idv_session_user.reproof_for_irs?(service_provider: current_sp) redirect_to idv_activated_url end @@ -29,20 +28,26 @@ def confirm_phone_or_address_confirmed def idv_session @idv_session ||= Idv::Session.new( user_session: user_session, - current_user: effective_user, + current_user: idv_session_user, service_provider: current_sp, ) end - def redirect_unless_effective_user - redirect_to root_url if !effective_user + def redirect_unless_idv_session_user + redirect_to root_url if !idv_session_user end def redirect_if_sp_context_needed return if sp_from_sp_session.present? return unless IdentityConfig.store.idv_sp_required - return if effective_user.profiles.any? + return if idv_session_user.profiles.any? redirect_to account_url end + + def idv_session_user + return User.find_by(id: session[:doc_capture_user_id]) if !current_user && hybrid_session? + + current_user + end end diff --git a/app/controllers/concerns/rate_limit_concern.rb b/app/controllers/concerns/rate_limit_concern.rb index 0d3b5ad5130..055539253bb 100644 --- a/app/controllers/concerns/rate_limit_concern.rb +++ b/app/controllers/concerns/rate_limit_concern.rb @@ -47,8 +47,7 @@ def throttle_and_controller_match(throttle_type) self.instance_of?(Idv::VerifyInfoController) || self.instance_of?(Idv::InPerson::VerifyInfoController) when :idv_doc_auth - self.instance_of?(Idv::DocumentCaptureController) || - self.instance_of?(Idv::HybridMobile::DocumentCaptureController) + self.instance_of?(Idv::DocumentCaptureController) when :proof_address self.instance_of?(Idv::PhoneController) end @@ -56,7 +55,7 @@ def throttle_and_controller_match(throttle_type) def idv_attempter_rate_limited?(throttle_type) Throttle.new( - user: effective_user, + user: idv_session_user, throttle_type: throttle_type, ).throttled? end diff --git a/app/controllers/idv/session_errors_controller.rb b/app/controllers/idv/session_errors_controller.rb index 64cf2659e91..b46d128caed 100644 --- a/app/controllers/idv/session_errors_controller.rb +++ b/app/controllers/idv/session_errors_controller.rb @@ -1,7 +1,6 @@ module Idv class SessionErrorsController < ApplicationController include IdvSession - include EffectiveUser include StepIndicatorConcern before_action :confirm_two_factor_authenticated_or_user_id_in_session @@ -15,7 +14,7 @@ def exception def warning throttle = Throttle.new( - user: effective_user, + user: idv_session_user, throttle_type: :idv_resolution, ) @@ -29,7 +28,7 @@ def state_id_warning def failure throttle = Throttle.new( - user: effective_user, + user: idv_session_user, throttle_type: :idv_resolution, ) @expires_at = throttle.expires_at @@ -53,7 +52,7 @@ def ssn_failure end def throttled - throttle = Throttle.new(user: effective_user, throttle_type: :idv_doc_auth) + throttle = Throttle.new(user: idv_session_user, throttle_type: :idv_doc_auth) log_event(based_on_throttle: throttle) @expires_at = throttle.expires_at end diff --git a/spec/controllers/concerns/rate_limit_concern_spec.rb b/spec/controllers/concerns/rate_limit_concern_spec.rb index 848d2904944..d49a1bef4f2 100644 --- a/spec/controllers/concerns/rate_limit_concern_spec.rb +++ b/spec/controllers/concerns/rate_limit_concern_spec.rb @@ -6,6 +6,7 @@ module Idv class StepController < ApplicationController include RateLimitConcern + include IdvSession def show render plain: 'Hello' diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb index 7224ba5d670..24bc01e3b87 100644 --- a/spec/controllers/idv/document_capture_controller_spec.rb +++ b/spec/controllers/idv/document_capture_controller_spec.rb @@ -102,6 +102,13 @@ end end + it 'does not use effective user outside of analytics_user in ApplicationControler' do + allow(subject).to receive(:analytics_user).and_return(subject.current_user) + expect(subject).not_to receive(:effective_user) + + get :show + end + context 'user is rate_limited' do it 'redirects to rate limited page' do user = create(:user) From e7e88a29ed4fc3487e2495e7640714100a0d60c8 Mon Sep 17 00:00:00 2001 From: Sonia Connolly Date: Thu, 1 Jun 2023 12:58:19 -0700 Subject: [PATCH 06/20] LG-9975 hybrid handoff redirects (#8520) * Use :js only where needed in hybrid_handoff feature specs * Remove unused methods from hybrid_handoff_controller * Add before_action that checks if flow_path is set * Add controller specs for before_action * Set flow_path to nil when redirecting to hybrid_handoff [skip changelog] * Fix prev PR: Redirect to document_capture instead of capture_complete After trying to go to link_sent in hybrid_mobile_spec * Check flag in document_capture redirect * With feature flag on, redirect from UploadStep back to DocumentCapture When UploadStep or HybridHandoff have already been completed. This is for the 50/50 state. --- .../idv/document_capture_controller.rb | 6 +++- .../idv/hybrid_handoff_controller.rb | 34 +++++++++++-------- app/controllers/idv/link_sent_controller.rb | 1 + .../idv/actions/cancel_link_sent_action.rb | 1 + .../actions/redo_document_capture_action.rb | 1 + app/services/idv/steps/agreement_step.rb | 4 +-- .../idv/document_capture_controller_spec.rb | 6 +--- .../idv/hybrid_handoff_controller_spec.rb | 28 +++++++++++++-- .../idv/doc_auth/document_capture_spec.rb | 8 ++++- .../idv/doc_auth/hybrid_handoff_spec.rb | 28 +++++++++++---- .../idv/hybrid_mobile/hybrid_mobile_spec.rb | 3 +- 11 files changed, 86 insertions(+), 34 deletions(-) diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index 9175f873079..044de86df5e 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -54,7 +54,11 @@ def extra_view_variables def confirm_upload_step_complete return if flow_session[:flow_path].present? - redirect_to idv_doc_auth_url + if IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled + redirect_to idv_hybrid_handoff_url + else + redirect_to idv_doc_auth_url + end end def confirm_document_capture_needed diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index 09459ebd55e..39594ce4c05 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -9,9 +9,9 @@ class HybridHandoffController < ApplicationController before_action :confirm_two_factor_authenticated before_action :render_404_if_hybrid_handoff_controller_disabled before_action :confirm_agreement_step_complete + before_action :confirm_hybrid_handoff_needed, only: :show def show - flow_session[:flow_path] = 'standard' analytics.idv_doc_auth_upload_visited(**analytics_arguments) Funnel::DocAuth::RegisterStep.new(current_user.id, sp_session[:issuer]).call( @@ -62,13 +62,16 @@ def handle_phone_submission if !failure_reason flow_session[:flow_path] = 'hybrid' redirect_to idv_link_sent_url + + # for the 50/50 state + flow_session['Idv::Steps::UploadStep'] = true else redirect_to idv_hybrid_handoff_url end - analytics.idv_doc_auth_upload_submitted( - **analytics_arguments.merge(form_response(destination: :link_sent).to_h), - ) + analytics_args = analytics_arguments.merge(form_response(destination: :link_sent).to_h) + analytics_args[:flow_path] = flow_session[:flow_path] + analytics.idv_doc_auth_upload_submitted(**analytics_args) end def send_link @@ -118,11 +121,13 @@ def bypass_send_link_steps flow_session[:flow_path] = 'standard' redirect_to idv_document_capture_url + # for the 50/50 state + flow_session['Idv::Steps::UploadStep'] = true + response = form_response(destination: :document_capture) analytics.idv_doc_auth_upload_submitted( **analytics_arguments.merge(response.to_h), ) - response end def extra_view_variables @@ -158,18 +163,9 @@ def analytics_arguments step: 'upload', analytics_id: 'Doc Auth', irs_reproofing: irs_reproofing?, - flow_path: flow_session[:flow_path], }.merge(**acuant_sdk_ab_test_analytics_args) end - def mark_link_sent_step_complete - flow_session['Idv::Steps::LinkSentStep'] = true - end - - def mark_upload_step_complete - flow_session['Idv::Steps::UploadStep'] = true - end - def form_response(destination:) FormResponse.new( success: true, @@ -220,6 +216,16 @@ def confirm_agreement_step_complete redirect_to idv_doc_auth_url end + def confirm_hybrid_handoff_needed + return if !flow_session[:flow_path] + + if flow_session[:flow_path] == 'standard' + redirect_to idv_document_capture_url + elsif flow_session[:flow_path] == 'hybrid' + redirect_to idv_link_sent_url + end + end + def formatted_destination_phone raw_phone = params.require(:doc_auth).permit(:phone) PhoneFormatter.format(raw_phone, country_code: 'US') diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb index ab7f362fbd1..a225ce44cd1 100644 --- a/app/controllers/idv/link_sent_controller.rb +++ b/app/controllers/idv/link_sent_controller.rb @@ -80,6 +80,7 @@ def handle_document_verification_success(get_results_response) def render_document_capture_cancelled if IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled redirect_to idv_hybrid_handoff_url + flow_session[:flow_path] = nil else mark_upload_step_incomplete redirect_to idv_doc_auth_url # was idv_url, why? diff --git a/app/services/idv/actions/cancel_link_sent_action.rb b/app/services/idv/actions/cancel_link_sent_action.rb index 66dc4ba49ae..60393229121 100644 --- a/app/services/idv/actions/cancel_link_sent_action.rb +++ b/app/services/idv/actions/cancel_link_sent_action.rb @@ -8,6 +8,7 @@ def self.analytics_submitted_event def call if IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled redirect_to idv_hybrid_handoff_url + flow_session[:flow_path] = nil else mark_step_incomplete(:upload) end diff --git a/app/services/idv/actions/redo_document_capture_action.rb b/app/services/idv/actions/redo_document_capture_action.rb index 2054bc349ce..40ffbe4aabe 100644 --- a/app/services/idv/actions/redo_document_capture_action.rb +++ b/app/services/idv/actions/redo_document_capture_action.rb @@ -11,6 +11,7 @@ def call redirect_to idv_document_capture_url elsif IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled redirect_to idv_hybrid_handoff_url + flow_session[:flow_path] = nil else mark_step_incomplete(:upload) end diff --git a/app/services/idv/steps/agreement_step.rb b/app/services/idv/steps/agreement_step.rb index e5eb9076bfc..21942060dd9 100644 --- a/app/services/idv/steps/agreement_step.rb +++ b/app/services/idv/steps/agreement_step.rb @@ -16,6 +16,7 @@ def call if flow_session[:skip_upload_step] redirect_to idv_document_capture_url + flow_session[:flow_path] = 'standard' else redirect_to idv_hybrid_handoff_url end @@ -30,9 +31,6 @@ def form_submit def skip_to_capture # See: Idv::DocAuthController#update_if_skipping_upload flow_session[:skip_upload_step] = true - if IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled - flow_session[:flow_path] = 'standard' - end end def consent_form_params diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb index 24bc01e3b87..1d7b7d5eebb 100644 --- a/spec/controllers/idv/document_capture_controller_spec.rb +++ b/spec/controllers/idv/document_capture_controller_spec.rb @@ -6,8 +6,7 @@ let(:flow_session) do { 'document_capture_session_uuid' => 'fd14e181-6fb1-4cdc-92e0-ef66dad0df4e', :threatmetrix_session_id => 'c90ae7a5-6629-4e77-b97c-f1987c2df7d0', - :flow_path => 'standard', - 'Idv::Steps::UploadStep' => true } + :flow_path => 'standard' } end let(:user) { create(:user) } @@ -19,9 +18,6 @@ ) end - let(:default_sdk_version) { IdentityConfig.store.idv_acuant_sdk_version_default } - let(:alternate_sdk_version) { IdentityConfig.store.idv_acuant_sdk_version_alternate } - before do allow(subject).to receive(:flow_session).and_return(flow_session) stub_sign_in(user) diff --git a/spec/controllers/idv/hybrid_handoff_controller_spec.rb b/spec/controllers/idv/hybrid_handoff_controller_spec.rb index 62fa39f37d2..a1ae8933685 100644 --- a/spec/controllers/idv/hybrid_handoff_controller_spec.rb +++ b/spec/controllers/idv/hybrid_handoff_controller_spec.rb @@ -28,13 +28,19 @@ :confirm_agreement_step_complete, ) end + + it 'checks that hybrid_handoff is needed' do + expect(subject).to have_actions( + :before, + :confirm_hybrid_handoff_needed, + ) + end end describe '#show' do let(:analytics_name) { 'IdV: doc auth upload visited' } let(:analytics_args) do - { flow_path: 'standard', - step: 'upload', + { step: 'upload', analytics_id: 'Doc Auth', irs_reproofing: false } end @@ -68,6 +74,24 @@ expect(response).to redirect_to(idv_doc_auth_url) end end + + context 'hybrid_handoff already visited' do + it 'redirects to document_capture in standard flow' do + subject.user_session['idv/doc_auth'][:flow_path] = 'standard' + + get :show + + expect(response).to redirect_to(idv_document_capture_url) + end + + it 'redirects to link_sent in hybrid flow' do + subject.user_session['idv/doc_auth'][:flow_path] = 'hybrid' + + get :show + + expect(response).to redirect_to(idv_link_sent_url) + end + end end describe '#update' do diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index bfc16edd4fb..87c3d502eea 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -35,7 +35,7 @@ end it 'shows the new DocumentCapture page for desktop standard flow' do - expect(page).to have_current_path(idv_document_capture_url) + expect(page).to have_current_path(idv_document_capture_path) expect(page).to have_content(t('doc_auth.headings.document_capture').tr(' ', ' ')) expect(page).to have_content(t('step_indicator.flows.idv.verify_id')) @@ -48,6 +48,12 @@ irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default, ) + + # it redirects here if trying to move earlier in the flow + visit(idv_doc_auth_agreement_step) + expect(page).to have_current_path(idv_document_capture_path) + visit(idv_doc_auth_upload_step) + expect(page).to have_current_path(idv_document_capture_path) end it 'logs return to sp link click' do diff --git a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb index a15d437ba52..92583e99ed4 100644 --- a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb +++ b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'doc auth upload step' do +feature 'doc auth hybrid_handoff step' do include IdvStepHelper include DocAuthHelper include ActionView::Helpers::DateHelper @@ -19,14 +19,24 @@ allow(IdentityConfig.store).to receive(:doc_auth_hybrid_handoff_controller_enabled). and_return(new_controller_enabled) allow_any_instance_of(Idv::HybridHandoffController).to receive(:mobile_device?).and_return(true) - complete_doc_auth_steps_before_upload_step allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) allow_any_instance_of(ApplicationController).to receive(:irs_attempts_api_tracker). and_return(fake_attempts_tracker) end - context 'on a desktop device', js: true do + it 'does not skip ahead in standard desktop flow' do + visit(idv_hybrid_handoff_url) + expect(page).to have_current_path(idv_doc_auth_welcome_step) + complete_welcome_step + visit(idv_hybrid_handoff_url) + expect(page).to have_current_path(idv_doc_auth_agreement_step) + complete_agreement_step + expect(page).to have_current_path(idv_hybrid_handoff_path) + end + + context 'on a desktop device' do before do + complete_doc_auth_steps_before_upload_step allow_any_instance_of( Idv::HybridHandoffController, ).to receive( @@ -54,9 +64,12 @@ 'IdV: doc auth upload submitted', hash_including(step: 'upload', destination: :document_capture), ) + + visit(idv_hybrid_handoff_url) + expect(page).to have_current_path(idv_document_capture_path) end - it "defaults phone to user's 2fa phone number" do + it "defaults phone to user's 2fa phone number", :js do field = page.find_field(t('two_factor_authentication.phone_label')) expect(field.value).to eq('(202) 555-1212') end @@ -73,9 +86,12 @@ 'IdV: doc auth upload submitted', hash_including(step: 'upload', destination: :link_sent), ) + + visit(idv_hybrid_handoff_url) + expect(page).to have_current_path(idv_link_sent_path) end - it 'proceeds to the next page with valid info' do + it 'proceeds to the next page with valid info', :js do expect(fake_attempts_tracker).to receive( :idv_phone_upload_link_sent, ).with( @@ -96,7 +112,7 @@ expect(page).to have_current_path(idv_link_sent_path) end - it 'does not proceed to the next page with invalid info' do + it 'does not proceed to the next page with invalid info', :js do fill_in :doc_auth_phone, with: '' click_send_link diff --git a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb index 1318d633099..c7ad3927831 100644 --- a/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb +++ b/spec/features/idv/hybrid_mobile/hybrid_mobile_spec.rb @@ -45,8 +45,7 @@ # Confirm that jumping to LinkSent page does not cause errors visit idv_link_sent_url expect(page).to have_current_path(root_url) - - visit idv_hybrid_mobile_capture_complete_url + visit idv_hybrid_mobile_document_capture_url attach_and_submit_images From 30ca396f2f7480cf317155db9550f42b0c2ff2ba Mon Sep 17 00:00:00 2001 From: Sonia Connolly Date: Fri, 2 Jun 2023 08:34:44 -0700 Subject: [PATCH 07/20] LG-9371 Add flow_path back to upload submitted analytics (#8528) * Add flow_path back to upload submitted analytics Analytics are sent separately from hybrid and standard code paths, and there were no specs on the standard path [skip changelog] * Use telephony_form_response for analytics when hybrid is chosen Confirmed by looking at UpdateStep logs that the telephony form response is added to analytics --- .../idv/hybrid_handoff_controller.rb | 16 +++--- .../idv/hybrid_handoff_controller_spec.rb | 49 ++++++++++++++----- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index 39594ce4c05..b174bc1e057 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -45,6 +45,7 @@ def handle_phone_submission return throttled_failure if throttle.throttled? idv_session.phone_for_mobile_flow = params[:doc_auth][:phone] flow_session[:phone_for_mobile_flow] = idv_session.phone_for_mobile_flow + flow_session[:flow_path] = 'hybrid' telephony_result = send_link telephony_form_response = build_telephony_form_response(telephony_result) @@ -60,18 +61,18 @@ def handle_phone_submission ) if !failure_reason - flow_session[:flow_path] = 'hybrid' redirect_to idv_link_sent_url # for the 50/50 state flow_session['Idv::Steps::UploadStep'] = true else redirect_to idv_hybrid_handoff_url + flow_session[:flow_path] = nil end - analytics_args = analytics_arguments.merge(form_response(destination: :link_sent).to_h) - analytics_args[:flow_path] = flow_session[:flow_path] - analytics.idv_doc_auth_upload_submitted(**analytics_args) + analytics.idv_doc_auth_upload_submitted( + **analytics_arguments.merge(telephony_form_response.to_h), + ) end def send_link @@ -103,6 +104,7 @@ def build_telephony_form_response(telephony_result) extra: { telephony_response: telephony_result.to_h, destination: :link_sent, + flow_path: flow_session[:flow_path], }, ) end @@ -124,9 +126,10 @@ def bypass_send_link_steps # for the 50/50 state flow_session['Idv::Steps::UploadStep'] = true - response = form_response(destination: :document_capture) analytics.idv_doc_auth_upload_submitted( - **analytics_arguments.merge(response.to_h), + **analytics_arguments.merge( + form_response(destination: :document_capture).to_h, + ), ) end @@ -173,6 +176,7 @@ def form_response(destination:) extra: { destination: destination, skip_upload_step: mobile_device?, + flow_path: flow_session[:flow_path], }, ) end diff --git a/spec/controllers/idv/hybrid_handoff_controller_spec.rb b/spec/controllers/idv/hybrid_handoff_controller_spec.rb index a1ae8933685..19e8b525529 100644 --- a/spec/controllers/idv/hybrid_handoff_controller_spec.rb +++ b/spec/controllers/idv/hybrid_handoff_controller_spec.rb @@ -96,21 +96,46 @@ describe '#update' do let(:analytics_name) { 'IdV: doc auth upload submitted' } - let(:analytics_args) do - { success: true, - errors: {}, - destination: :link_sent, - flow_path: 'hybrid', - step: 'upload', - analytics_id: 'Doc Auth', - irs_reproofing: false, - skip_upload_step: false } + + context 'hybrid flow' do + let(:analytics_args) do + { success: true, + errors: { message: nil }, + destination: :link_sent, + flow_path: 'hybrid', + step: 'upload', + analytics_id: 'Doc Auth', + irs_reproofing: false, + telephony_response: { errors: {}, + message_id: 'fake-message-id', + request_id: 'fake-message-request-id', + success: true } } + end + + it 'sends analytics_submitted event for hybrid' do + put :update, params: { doc_auth: { phone: '202-555-5555' } } + + expect(@analytics).to have_logged_event(analytics_name, analytics_args) + end end - it 'sends analytics_submitted event' do - put :update, params: { doc_auth: { phone: '202-555-5555' } } + context 'desktop flow' do + let(:analytics_args) do + { success: true, + errors: {}, + destination: :document_capture, + flow_path: 'standard', + step: 'upload', + analytics_id: 'Doc Auth', + irs_reproofing: false, + skip_upload_step: false } + end - expect(@analytics).to have_logged_event(analytics_name, analytics_args) + it 'sends analytics_submitted event for desktop' do + put :update, params: { type: 'desktop' } + + expect(@analytics).to have_logged_event(analytics_name, analytics_args) + end end end end From 8262c240245450e0c81b647539ad008e0c6660f7 Mon Sep 17 00:00:00 2001 From: Eric Gade <105373963+eric-gade@users.noreply.github.com> Date: Fri, 2 Jun 2023 12:57:08 -0400 Subject: [PATCH 08/20] LG-9871 Password Re-entry Content Changes (#8508) * Adding info localization, banner, and test update * Updating locales and view with new body text * Updating locales for the header * Removing help article link and information accordion * Bringing feature spec in line with removals * Using get_a_letter indicator step for GPO path changelog: User-Facing Improvements, Review step, updating content on idv review / password reentry step Co-authored-by: Andrew Duthie --- app/controllers/idv/review_controller.rb | 12 +++++- app/views/idv/review/new.html.erb | 16 ++------ config/locales/idv/en.yml | 7 +++- config/locales/idv/es.yml | 7 +++- config/locales/idv/fr.yml | 9 +++-- .../controllers/idv/review_controller_spec.rb | 19 ++++++++- spec/features/idv/in_person_spec.rb | 2 +- spec/features/idv/steps/review_step_spec.rb | 10 +---- spec/views/idv/review/new.html.erb_spec.rb | 40 +------------------ 9 files changed, 50 insertions(+), 72 deletions(-) diff --git a/app/controllers/idv/review_controller.rb b/app/controllers/idv/review_controller.rb index d7912a0be5e..2cb7ad61914 100644 --- a/app/controllers/idv/review_controller.rb +++ b/app/controllers/idv/review_controller.rb @@ -11,6 +11,8 @@ class ReviewController < ApplicationController before_action :confirm_address_step_complete before_action :confirm_current_password, only: [:create] + helper_method :step_indicator_step + rescue_from UspsInPersonProofing::Exception::RequestEnrollException, with: :handle_request_enroll_exception @@ -30,7 +32,6 @@ def confirm_current_password end def new - @applicant = idv_session.applicant Funnel::DocAuth::RegisterStep.new(current_user.id, current_sp&.issuer). call(:encrypt, :view, true) analytics.idv_review_info_visited(address_verification_method: address_verification_method) @@ -41,6 +42,8 @@ def new flash_now[:error] = t('idv.errors.mail_limit_reached') elsif idv_session.phone_confirmed? flash_now[:success] = t('idv.messages.review.phone_verified') + elsif address_verification_method == 'gpo' + flash_now[:info] = t('idv.messages.review.gpo_pending') end end @@ -74,10 +77,15 @@ def create session[:last_gpo_confirmation_code] = idv_session.gpo_otp end + def step_indicator_step + return :secure_account unless address_verification_method == 'gpo' + :get_a_letter + end + private def address_verification_method - user_session.dig('idv', 'address_verification_mechanism') + user_session.with_indifferent_access.dig('idv', 'address_verification_mechanism') end def init_profile diff --git a/app/views/idv/review/new.html.erb b/app/views/idv/review/new.html.erb index 192a21354dc..53a36869575 100644 --- a/app/views/idv/review/new.html.erb +++ b/app/views/idv/review/new.html.erb @@ -3,7 +3,7 @@ <% content_for(:pre_flash_content) do %> <%= render StepIndicatorComponent.new( steps: step_indicator_steps, - current_step: :secure_account, + current_step: step_indicator_step, locale_scope: 'idv', class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', ) %> @@ -12,14 +12,9 @@ <%= render PageHeadingComponent.new.with_content(t('idv.titles.session.review', app_name: APP_NAME)) %>

- <%= t('idv.messages.sessions.review_message', app_name: APP_NAME) %> + <%= t('idv.messages.review.message', app_name: APP_NAME) %>

-<%= new_tab_link_to( - t('idv.messages.sessions.read_more_encrypt', app_name: APP_NAME), - MarketingSite.security_url, - ) %> - <%= simple_form_for( current_user, url: idv_review_path, @@ -35,14 +30,9 @@ }, }, ) %> -
+
<%= link_to(t('idv.forgot_password.link_text'), idv_forgot_password_url, class: 'margin-left-1') %>
- <%= render AccordionComponent.new do |c| %> - <% c.with_header { t('idv.messages.review.intro') } %> - <%= render 'shared/pii_review', pii: @applicant, - phone: PhoneFormatter.format(@applicant[:phone]) %> - <% end %> <%= f.submit t('forms.buttons.continue'), class: 'margin-top-5' %> <% end %> diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml index 0ee13be812f..c3d65ce8ef7 100644 --- a/config/locales/idv/en.yml +++ b/config/locales/idv/en.yml @@ -215,7 +215,11 @@ en: - Your primary number (the one you use the most often) return_to_profile: '‹ Return to your %{app_name} profile' review: + gpo_pending: We’ll send your letter once you re-enter your password. intro: Your verified information + message: '%{app_name} will encrypt your information with your password. This + means that your information is secure and only you will be able to + access or change it.' phone_verified: We verified your phone number select_verification_with_sp: To protect you from identity fraud, we will contact you to confirm that this %{sp_name} account is legitimate. @@ -224,7 +228,6 @@ en: sessions: no_pii: TEST SITE - Do not use real personal information (demo purposes only) - TEST SITE - read_more_encrypt: Read more about how %{app_name} protects your personal information review_message: When you re-enter your password, %{app_name} will protect the information you’ve given us, so that only you can access it. verifying: Verifying… @@ -242,7 +245,7 @@ en: otp_delivery_method: How should we send a code? review: Review and submit session: - review: Re-enter your %{app_name} password to protect your data + review: Re-enter your %{app_name} password unavailable: 'We are working to resolve an error' troubleshooting: headings: diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml index 636f3519321..ba4fa51cbf2 100644 --- a/config/locales/idv/es.yml +++ b/config/locales/idv/es.yml @@ -229,7 +229,11 @@ es: - Su número principal (el que utiliza con más frecuencia) return_to_profile: '‹ Volver a tu perfil de %{app_name}' review: + gpo_pending: Enviaremos tu carta una vez que hayas ingresado tu contraseña. intro: Su información verificada + message: '%{app_name} encriptará tu información con tu contraseña. Esto + significa que tu información estará segura y solo tú podrás + consultarla o modificarla.' phone_verified: Verificamos su número de teléfono select_verification_with_sp: Para protegerlo de robo de identidad, no puede utilizar su cuenta en %{sp_name} hasta que la active ingresando un @@ -239,7 +243,6 @@ es: sessions: no_pii: SITIO DE PRUEBA - No utilice información personal real (sólo para propósitos de demostración) - SITIO DE PRUEBA - read_more_encrypt: Lea más sobre cómo %{app_name} protege su información personal review_message: Cuando vuelva a ingresar su contraseña, %{app_name} cifrará sus datos para asegurarse de que nadie más pueda acceder a ellos. verifying: Verificando… @@ -257,7 +260,7 @@ es: otp_delivery_method: '¿Cómo debemos enviar un código?' review: Revise y envíe session: - review: Vuelve a ingresar tu contraseña de %{app_name} para encriptar tus datos + review: 'Vuelve a ingresar tu contraseña de %{app_name}' unavailable: Estamos trabajando para resolver un error troubleshooting: headings: diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml index d32f10c8eb0..b0550377cfc 100644 --- a/config/locales/idv/fr.yml +++ b/config/locales/idv/fr.yml @@ -243,7 +243,12 @@ fr: - Votre numéro principal (celui que vous utilisez le plus souvent) return_to_profile: '‹ Revenir à votre profil %{app_name}' review: + gpo_pending: Nous vous enverrons votre lettre une fois que vous aurez + réintroduit votre mot de passe. intro: Vos informations vérifiées + message: '%{app_name} crypte vos informations avec votre mot de passe. Cela + signifie que vos informations sont sécurisées et que vous seul pourrez + y accéder ou les modifier.' phone_verified: Nous avons vérifié votre numéro de téléphone select_verification_with_sp: Afin de vous protéger des fraudes d’identité, vous ne pouvez pas utiliser votre compte au %{sp_name} tant que vous ne @@ -254,8 +259,6 @@ fr: sessions: no_pii: SITE DE TEST - N’utilisez pas de véritables données personnelles (il s’agit d’une démonstration seulement) - SITE DE TEST - read_more_encrypt: En savoir plus sur la façon dont %{app_name} protège vos - informations personnelles review_message: Lorsque vous entrez à nouveau votre mot de passe, %{app_name} crypte vos données pour vous assurer que personne ne peut y accéder. verifying: Vérification… @@ -273,7 +276,7 @@ fr: otp_delivery_method: Comment envoyer un code? review: Réviser et soumettre session: - review: Entrez à nouveau votre mot de passe %{app_name} pour crypter vos données + review: 'Saisissez à nouveau votre mot de passe %{app_name}' unavailable: Nous travaillons à la résolution d’une erreur troubleshooting: headings: diff --git a/spec/controllers/idv/review_controller_spec.rb b/spec/controllers/idv/review_controller_spec.rb index 8e714d7a63f..97d4714ab48 100644 --- a/spec/controllers/idv/review_controller_spec.rb +++ b/spec/controllers/idv/review_controller_spec.rb @@ -159,14 +159,31 @@ def show ) end + it 'uses the correct step indicator step' do + indicator_step = subject.step_indicator_step + + expect(indicator_step).to eq(:secure_account) + end + context 'user is in gpo flow' do - it 'does not display success message' do + before do idv_session.vendor_phone_confirmation = false idv_session.address_verification_mechanism = 'gpo' + end + it 'displays info message about sending letter' do get :new expect(flash.now[:success]).to be_nil + expect(flash.now[:info]).to eq( + t('idv.messages.review.gpo_pending'), + ) + end + + it 'uses the correct step indicator step' do + indicator_step = subject.step_indicator_step + + expect(indicator_step).to eq(:get_a_letter) end end diff --git a/spec/features/idv/in_person_spec.rb b/spec/features/idv/in_person_spec.rb index 1e370990a92..d5dbbace4d5 100644 --- a/spec/features/idv/in_person_spec.rb +++ b/spec/features/idv/in_person_spec.rb @@ -388,7 +388,7 @@ t('step_indicator.flows.idv.verify_phone_or_address'), ) click_on t('idv.buttons.mail.send') - expect_in_person_gpo_step_indicator_current_step(t('step_indicator.flows.idv.secure_account')) + expect_in_person_gpo_step_indicator_current_step(t('step_indicator.flows.idv.get_a_letter')) complete_review_step expect_in_person_gpo_step_indicator_current_step(t('step_indicator.flows.idv.get_a_letter')) diff --git a/spec/features/idv/steps/review_step_spec.rb b/spec/features/idv/steps/review_step_spec.rb index 0a5f9b5eacb..f781d55951a 100644 --- a/spec/features/idv/steps/review_step_spec.rb +++ b/spec/features/idv/steps/review_step_spec.rb @@ -13,15 +13,7 @@ start_idv_from_sp complete_idv_steps_before_review_step - click_on t('idv.messages.review.intro') - - expect(page).to have_content('FAKEY') - expect(page).to have_content('MCFAKERSON') - expect(page).to have_content('1 FAKE RD') - expect(page).to have_content('GREAT FALLS, MT 59010') - expect(page).to have_content('October 06, 1938') - expect(page).to have_content(DocAuthHelper::GOOD_SSN) - expect(page).to have_content('+1 202-555-1212') + expect(page).to have_content(t('idv.messages.review.message', app_name: APP_NAME)) fill_in 'Password', with: 'this is not the right password' click_idv_continue diff --git a/spec/views/idv/review/new.html.erb_spec.rb b/spec/views/idv/review/new.html.erb_spec.rb index 53e7c6b7892..aee5227aafb 100644 --- a/spec/views/idv/review/new.html.erb_spec.rb +++ b/spec/views/idv/review/new.html.erb_spec.rb @@ -11,58 +11,20 @@ allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:step_indicator_steps). and_return(Idv::Flows::DocAuthFlow::STEP_INDICATOR_STEPS) - @applicant = { - first_name: 'Some', - last_name: 'One', - ssn: '666-66-1234', - dob: dob, - address1: '123 Main St', - city: 'Somewhere', - state: 'MO', - zipcode: '12345', - phone: '+1 (213) 555-0000', - } + allow(view).to receive(:step_indicator_step).and_return(:secure_account) render end - it 'renders all steps' do - expect(rendered).to have_content('Some One') - expect(rendered).to have_content('123 Main St') - expect(rendered).to have_content('Somewhere') - expect(rendered).to have_content('MO') - expect(rendered).to have_content('12345') - expect(rendered).to have_content('666-66-1234') - expect(rendered).to have_content('+1 213-555-0000') - expect(rendered).to have_content('March 29, 1972') - end - it 'renders the correct content heading' do expect(rendered).to have_content t('idv.titles.session.review', app_name: APP_NAME) end - it 'contains an accordion with verified user information' do - accordion_selector = generate_class_selector('usa-accordion') - expect(rendered).to have_xpath("//#{accordion_selector}") - end - - it 'renders the correct header for the accordion' do - expect(rendered).to have_content(t('idv.messages.review.intro')) - end - it 'shows the step indicator' do expect(view.content_for(:pre_flash_content)).to have_css( '.step-indicator__step--current', text: t('step_indicator.flows.idv.secure_account'), ) end - - context 'with an american-style dob' do - let(:dob) { '12/31/1970' } - - it 'renders correctly' do - expect(rendered).to have_selector('.h4.text-bold', text: 'December 31, 1970') - end - end end end From e7cb0a0bb240e50d536221877206d085aa9e01cb Mon Sep 17 00:00:00 2001 From: Eric Gade <105373963+eric-gade@users.noreply.github.com> Date: Fri, 2 Jun 2023 13:11:15 -0400 Subject: [PATCH 09/20] LG-9641 GPO Resend Confirm Interstitial (#8527) * Updating locales and gpo template * Updating feature test for re-send case * Switching to use of StatusPageComponent We are reusing the gpo_controller aka "Want a letter?" code to create this interstitial page. changelog: User-Facing Improvements, Confirmation page, add interstitial confirming that the user wants another letter sent --- app/views/idv/gpo/index.html.erb | 26 ++++++++++++++++-------- config/locales/idv/en.yml | 6 +++++- config/locales/idv/es.yml | 7 ++++++- config/locales/idv/fr.yml | 8 +++++++- spec/features/idv/steps/gpo_step_spec.rb | 9 ++++++++ 5 files changed, 45 insertions(+), 11 deletions(-) diff --git a/app/views/idv/gpo/index.html.erb b/app/views/idv/gpo/index.html.erb index 69ec0f3f07b..58664779d38 100644 --- a/app/views/idv/gpo/index.html.erb +++ b/app/views/idv/gpo/index.html.erb @@ -9,14 +9,24 @@ ) %> <% end %> -<%= render AlertComponent.new(message: t('idv.messages.gpo.info_alert'), class: 'margin-bottom-4') %> - -<%= render PageHeadingComponent.new.with_content(@presenter.title) %> - -

- <%= t('idv.messages.gpo.address_on_file_html') %> - <%= t('idv.messages.gpo.timeframe_html') %> -

+<% if @presenter.resend_requested? %> + <%= render StatusPageComponent.new(status: :warning) do |c| %> + <% c.with_header { @presenter.title } %> +

+ <%= t('idv.messages.gpo.resend_timeframe') %> +

+

+ <%= t('idv.messages.gpo.resend_code_warning') %> +

+ <% end %> +<% else %> + <%= render AlertComponent.new(message: t('idv.messages.gpo.info_alert'), class: 'margin-bottom-4') %> + <%= render PageHeadingComponent.new.with_content(@presenter.title) %> +

+ <%= t('idv.messages.gpo.address_on_file_html') %> + <%= t('idv.messages.gpo.timeframe_html') %> +

+<% end %>
<%= button_to @presenter.button, diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml index c3d65ce8ef7..ca19f31e4de 100644 --- a/config/locales/idv/en.yml +++ b/config/locales/idv/en.yml @@ -200,6 +200,10 @@ en: info_alert: You’ll need to wait until your letter is delivered to finish verifying your identity. resend: Send me another letter + resend_code_warning: If you request a new letter now, the one-time code from + your current letter will remain valid for a limited time. You may + still use the one-time code from either letter. + resend_timeframe: Letters typically take 3 to 7 business days to arrive. timeframe_html: Letters are sent the next business day via USPS First Class Mail and typically take 3 to 7 business days to arrive. otp_delivery_method_description: If you entered a landline above, please select “Phone call” below. @@ -240,7 +244,7 @@ en: activated: Your identity has already been verified come_back_later: Your letter is on the way mail: - resend: Want another letter? + resend: Send another letter? verify: Want a letter? otp_delivery_method: How should we send a code? review: Review and submit diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml index ba4fa51cbf2..f2368f24d79 100644 --- a/config/locales/idv/es.yml +++ b/config/locales/idv/es.yml @@ -212,6 +212,11 @@ es: info_alert: Tendrá que esperar a que su carta sea entregada para terminar de verificar su identidad. resend: Envíeme otra carta + resend_code_warning: Si solicitas una nueva carta ahora, el código de una sola + vez de tu carta actual seguirá siendo válido durante un tiempo + limitado. Puedes seguir utilizando el código de una sola vez de + cualquiera de las dos cartas. + resend_timeframe: Las cartas suelen tardar entre 3 y 7 días hábiles en llegar. timeframe_html: Las cartas se envían al día siguiente por First Class Mail de USPS y suelen tardar entre 3 y 7 días hábiles en llegar. @@ -255,7 +260,7 @@ es: activated: Ya se verificó tu identidad. come_back_later: Su carta está en camino mail: - resend: '¿Desea otra carta?' + resend: '¿Enviar otra carta?' verify: '¿Desea una carta?' otp_delivery_method: '¿Cómo debemos enviar un código?' review: Revise y envíe diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml index b0550377cfc..5c7316cb18c 100644 --- a/config/locales/idv/fr.yml +++ b/config/locales/idv/fr.yml @@ -225,6 +225,12 @@ fr: info_alert: Vous devrez attendre la livraison de votre lettre pour compléter la vérification de votre identité. resend: Envoyez-moi une autre lettre + resend_code_warning: Si vous demandez une nouvelle lettre maintenant, le code à + usage unique de votre lettre actuelle restera valable pendant une + période limitée. Vous pouvez toujours utiliser le code à usage unique + de l’une ou l’autre lettre. + resend_timeframe: Les lettres mettent généralement entre trois et sept jours + ouvrables pour arriver. timeframe_html: Les lettres sont envoyées les jours ouvrables par courriel de première classe de USPS et prennent généralement entre trois à sept jours ouvrables pour être reçues. @@ -271,7 +277,7 @@ fr: activated: Votre identité a déjà été vérifiée come_back_later: Votre lettre est en route mail: - resend: Vous voulez une autre lettre? + resend: Envoyer une autre lettre? verify: Vous voulez une lettre? otp_delivery_method: Comment envoyer un code? review: Réviser et soumettre diff --git a/spec/features/idv/steps/gpo_step_spec.rb b/spec/features/idv/steps/gpo_step_spec.rb index 882c561ff6a..81839f8a57d 100644 --- a/spec/features/idv/steps/gpo_step_spec.rb +++ b/spec/features/idv/steps/gpo_step_spec.rb @@ -36,9 +36,18 @@ it 'allows the user to resend a letter and redirects to the come back later step' do complete_idv_and_return_to_gpo_step + # Confirm that we show the correct content on + # the GPO page for users requesting re-send + expect(page).to have_content(t('idv.titles.mail.resend')) + expect(page).to have_content(t('idv.messages.gpo.resend_timeframe')) + expect(page).to have_content(t('idv.messages.gpo.resend_code_warning')) + expect(page).to have_content(t('idv.buttons.mail.resend')) + expect(page).to_not have_content(t('idv.messages.gpo.info_alert')) + expect { click_on t('idv.buttons.mail.resend') }. to change { GpoConfirmation.count }.from(1).to(2) expect_user_to_be_unverified(user) + expect(page).to have_content(t('idv.titles.come_back_later')) expect(page).to have_current_path(idv_come_back_later_path) From 35dffd670c5a89595285aaae783450878e3be884 Mon Sep 17 00:00:00 2001 From: Kimball Bighorse Date: Fri, 2 Jun 2023 10:32:31 -0700 Subject: [PATCH 10/20] changelog: User-Facing Improvements, Identity Verification, Remove 404 on hybrid handoff (#8500) --- app/controllers/idv/hybrid_handoff_controller.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index b174bc1e057..fdf07d77a04 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -7,7 +7,6 @@ class HybridHandoffController < ApplicationController include StepUtilitiesConcern before_action :confirm_two_factor_authenticated - before_action :render_404_if_hybrid_handoff_controller_disabled before_action :confirm_agreement_step_complete before_action :confirm_hybrid_handoff_needed, only: :show @@ -210,10 +209,6 @@ def failure(message, extra = nil) FormResponse.new(**form_response_params) end - def render_404_if_hybrid_handoff_controller_disabled - render_not_found unless IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled - end - def confirm_agreement_step_complete return if flow_session['Idv::Steps::AgreementStep'] From bbb17ac84547dcb9022043b47eff5858fe802c9a Mon Sep 17 00:00:00 2001 From: Matt Hinz Date: Fri, 2 Jun 2023 11:54:54 -0700 Subject: [PATCH 11/20] LG-9848: Add analytics to onbeforeunload (#8512) * Analytics for doc capture polling onbeforeunload * Refactor promptOnNavigate logic out into a package Set an onbeforeunload handler, do some analytics, manage cleanup. * Ping 5, 15, and 30s after onbeforeunload * remove stray extra newline * Remove peerDependency from prompt-on-navigate package * Remove stray file * Don't use a separate var to track whether promptOnNavigate was bound * Use window.setTimeout / window.clearTimeout Avoid strange-looking `NodeJS.Timeout` type * Lint * Update doc capture polling tests to clean up Don't leave onbeforeunload handlers hanging around * changelog: Internal, Analytics, Add analytics to onbeforeunload handlers * Put event names in variables * Add README for prompt-on-navigate package * Update events + add to FrontendLogController * Update tests to reference event name variables * Fake window.setTimeout etc. with useFakeTimers Sinon's useFakeTimers covers global.setTimeout (which is callable as plain `setTimeout(...)`, but does not cover `window.setTimeout`. --- app/controllers/frontend_log_controller.rb | 2 + .../document-capture-polling/index.ts | 18 ++- .../packages/form-steps/prompt-on-navigate.ts | 13 +- .../packages/prompt-on-navigate/README.md | 28 ++++ .../packages/prompt-on-navigate/index.spec.ts | 144 ++++++++++++++++++ .../packages/prompt-on-navigate/index.ts | 70 +++++++++ .../packages/prompt-on-navigate/package.json | 5 + .../packages/test-helpers/use-sandbox.ts | 24 +++ app/services/analytics_events.rb | 19 +++ .../document-capture-polling/index-spec.js | 4 + 10 files changed, 310 insertions(+), 17 deletions(-) create mode 100644 app/javascript/packages/prompt-on-navigate/README.md create mode 100644 app/javascript/packages/prompt-on-navigate/index.spec.ts create mode 100644 app/javascript/packages/prompt-on-navigate/index.ts create mode 100644 app/javascript/packages/prompt-on-navigate/package.json diff --git a/app/controllers/frontend_log_controller.rb b/app/controllers/frontend_log_controller.rb index 589a6152a57..3c4b4d19652 100644 --- a/app/controllers/frontend_log_controller.rb +++ b/app/controllers/frontend_log_controller.rb @@ -22,6 +22,8 @@ class FrontendLogController < ApplicationController 'IdV: user clicked sp link on ready to verify page' => :idv_in_person_ready_to_verify_sp_link_clicked, 'IdV: user clicked what to bring link on ready to verify page' => :idv_in_person_ready_to_verify_what_to_bring_link_clicked, 'IdV: consent checkbox toggled' => :idv_consent_checkbox_toggled, + 'User prompted before navigation' => :user_prompted_before_navigation, + 'User prompted before navigation and still on page' => :user_prompted_before_navigation_and_still_on_page, }.transform_values { |method| AnalyticsEvents.instance_method(method) }.freeze # rubocop:enable Layout/LineLength diff --git a/app/javascript/packages/document-capture-polling/index.ts b/app/javascript/packages/document-capture-polling/index.ts index 384d8db0afe..e9329e35125 100644 --- a/app/javascript/packages/document-capture-polling/index.ts +++ b/app/javascript/packages/document-capture-polling/index.ts @@ -1,4 +1,5 @@ import { trackEvent as defaultTrackEvent } from '@18f/identity-analytics'; +import { promptOnNavigate } from '@18f/identity-prompt-on-navigate'; export const DOC_CAPTURE_TIMEOUT = 1000 * 60 * 25; // 25 minutes export const DOC_CAPTURE_POLL_INTERVAL = 5000; @@ -44,6 +45,8 @@ export class DocumentCapturePolling { pollAttempts = 0; + cleanUpPromptOnNavigate: (() => void) | undefined; + constructor({ elements, statusEndpoint, @@ -70,12 +73,15 @@ export class DocumentCapturePolling { * @param {boolean} shouldPrompt Whether to bind or unbind page unload behavior. */ bindPromptOnNavigate(shouldPrompt) { - window.onbeforeunload = shouldPrompt - ? (event) => { - event.preventDefault(); - event.returnValue = ''; - } - : null; + const isAlreadyBound = !!this.cleanUpPromptOnNavigate; + + if (shouldPrompt && !isAlreadyBound) { + this.cleanUpPromptOnNavigate = promptOnNavigate(); + } else if (!shouldPrompt && isAlreadyBound) { + const cleanUp = this.cleanUpPromptOnNavigate ?? (() => {}); + this.cleanUpPromptOnNavigate = undefined; + cleanUp(); + } } onMaxPollAttempts() { diff --git a/app/javascript/packages/form-steps/prompt-on-navigate.ts b/app/javascript/packages/form-steps/prompt-on-navigate.ts index 00b198d70b1..581f5f345af 100644 --- a/app/javascript/packages/form-steps/prompt-on-navigate.ts +++ b/app/javascript/packages/form-steps/prompt-on-navigate.ts @@ -1,4 +1,5 @@ import { useLayoutEffect } from 'react'; +import { promptOnNavigate } from '@18f/identity-prompt-on-navigate'; /** * While mounted, prompts the user to confirm navigation. @@ -7,17 +8,7 @@ function PromptOnNavigate() { // Use `useLayoutEffect` to guarantee that event unbinding occurs synchronously. // // See: https://reactjs.org/blog/2020/08/10/react-v17-rc.html#effect-cleanup-timing - useLayoutEffect(() => { - function onBeforeUnload(event) { - event.preventDefault(); - event.returnValue = ''; - } - - window.onbeforeunload = onBeforeUnload; - return () => { - window.onbeforeunload = null; - }; - }); + useLayoutEffect(promptOnNavigate); return null; } diff --git a/app/javascript/packages/prompt-on-navigate/README.md b/app/javascript/packages/prompt-on-navigate/README.md new file mode 100644 index 00000000000..085ec401180 --- /dev/null +++ b/app/javascript/packages/prompt-on-navigate/README.md @@ -0,0 +1,28 @@ +# `@18f/identity-prompt-on-navigate` + +Configures an `onbeforeunload` event handler such that the browser will prompt the user before they reload or navigate away from the page. + +## Usage + +```js +import { promptOnNavigate } from "@18f/identity-prompt-on-navigate"; + +// Set the onbeforeunload event handler. +const cleanUp = promptOnNavigate(); + +// ...some time later, call cleanUp() to restore any previous onbeforeunload handler and cancel any pending timers (this is important). +cleanUp(); +``` + +## Analytics + +By default, `promptOnNavigate` will call `trackEvent` to log a `User prompted before navigation` event when the onbeforeunload handler is called. It will then log `User prompted before navigation and still on page` events at 5, 15, and 30 seconds after the onbeforeunload handler is called (the `seconds` property on the event will contain the number of seconds since the initial prompt). + +You can customize these intervals by passing a `stillOnPageIntervalsInSeconds` option: + +```js +promptOnNavigate({ + // Log a 'User prompted before navigation and still on page' event 7 and 11 seconds after the initial prompt. + stillOnPageIntervalsInSeconds: [7, 11], +}); +``` diff --git a/app/javascript/packages/prompt-on-navigate/index.spec.ts b/app/javascript/packages/prompt-on-navigate/index.spec.ts new file mode 100644 index 00000000000..bbd4c0bbc48 --- /dev/null +++ b/app/javascript/packages/prompt-on-navigate/index.spec.ts @@ -0,0 +1,144 @@ +import { useSandbox } from '@18f/identity-test-helpers'; +import * as analytics from '@18f/identity-analytics'; +import { PROMPT_EVENT, STILL_ON_PAGE_EVENT, promptOnNavigate } from '.'; + +describe('promptOnNavigate', () => { + const sandbox = useSandbox({ useFakeTimers: true }); + + it('prompts on navigate', () => { + promptOnNavigate(); + + const event = new window.Event('beforeunload', { cancelable: true, bubbles: false }); + window.dispatchEvent(event); + + expect(event.defaultPrevented).to.be.true(); + expect(event.returnValue).to.be.false(); + }); + + it('logs an event', () => { + const trackEvent = sandbox.spy(analytics, 'trackEvent'); + + promptOnNavigate(); + + const event = new window.Event('beforeunload', { cancelable: true, bubbles: false }); + window.dispatchEvent(event); + + expect(trackEvent).to.have.been.calledOnceWith(PROMPT_EVENT); + }); + + it('logs a second event when the user stays on the page for 5s', () => { + const trackEvent = sandbox.spy(analytics, 'trackEvent'); + + promptOnNavigate(); + + const event = new window.Event('beforeunload', { cancelable: true, bubbles: false }); + + window.dispatchEvent(event); + + expect(trackEvent).to.have.been.calledOnceWith(PROMPT_EVENT); + trackEvent.resetHistory(); + + sandbox.clock.tick(2000); + expect(trackEvent).not.to.have.been.called(); + + sandbox.clock.tick(3000); + expect(trackEvent).to.have.been.calledWith(STILL_ON_PAGE_EVENT, { + seconds: 5, + }); + }); + + it('logs a third event when the user stays on the page for 15s', () => { + const trackEvent = sandbox.spy(analytics, 'trackEvent'); + + promptOnNavigate(); + + const event = new window.Event('beforeunload', { cancelable: true, bubbles: false }); + + window.dispatchEvent(event); + + expect(trackEvent).to.have.been.calledOnceWith(PROMPT_EVENT); + trackEvent.resetHistory(); + + sandbox.clock.tick(5000); + expect(trackEvent).to.have.been.called(); + trackEvent.resetHistory(); + + sandbox.clock.tick(10000); + expect(trackEvent).to.have.been.calledWith(STILL_ON_PAGE_EVENT, { + seconds: 15, + }); + }); + + it('logs a fourth event when the user stays on the page for 30s', () => { + const trackEvent = sandbox.spy(analytics, 'trackEvent'); + + promptOnNavigate(); + + const event = new window.Event('beforeunload', { cancelable: true, bubbles: false }); + + window.dispatchEvent(event); + + expect(trackEvent).to.have.been.called(); + trackEvent.resetHistory(); + + sandbox.clock.tick(5000); + expect(trackEvent).to.have.been.called(); + trackEvent.resetHistory(); + + sandbox.clock.tick(10000); + expect(trackEvent).to.have.been.called(); + trackEvent.resetHistory(); + + sandbox.clock.tick(15000); + expect(trackEvent).to.have.been.calledWith(STILL_ON_PAGE_EVENT, { + seconds: 30, + }); + }); + + it('cleans up after itself', () => { + window.onbeforeunload = null; + + const cleanup = promptOnNavigate(); + + expect(window.onbeforeunload).not.to.be.null(); + + cleanup(); + + expect(window.onbeforeunload).to.be.null(); + }); + + it("does not clean up someone else's handler", () => { + const clean = promptOnNavigate(); + const custom = () => {}; + window.onbeforeunload = custom; + clean(); + expect(window.onbeforeunload).to.eql(custom); + }); + + it('does not fire second analytics event after cleanup', () => { + const trackEvent = sandbox.spy(analytics, 'trackEvent'); + + const cleanup = promptOnNavigate(); + + const event = new window.Event('beforeunload', { cancelable: true, bubbles: false }); + window.dispatchEvent(event); + + expect(trackEvent).to.have.been.calledOnceWith(PROMPT_EVENT); + trackEvent.resetHistory(); + + sandbox.clock.tick(2000); + expect(trackEvent).not.to.have.been.called(); + + cleanup(); + + sandbox.clock.tick(10000); + expect(trackEvent).not.to.have.been.called(); + }); + + it('does not throw if you call cleanup a bunch', () => { + const cleanup = promptOnNavigate(); + for (let i = 0; i < 10; i++) { + cleanup(); + } + }); +}); diff --git a/app/javascript/packages/prompt-on-navigate/index.ts b/app/javascript/packages/prompt-on-navigate/index.ts new file mode 100644 index 00000000000..60461e35c86 --- /dev/null +++ b/app/javascript/packages/prompt-on-navigate/index.ts @@ -0,0 +1,70 @@ +import { trackEvent } from '@18f/identity-analytics'; + +export type PromptOnNavigateOptions = { + stillOnPageIntervalsInSeconds: number[]; +}; + +const defaults = { + stillOnPageIntervalsInSeconds: [5, 15, 30], +}; + +export const PROMPT_EVENT = 'User prompted before navigation'; + +export const STILL_ON_PAGE_EVENT = 'User prompted before navigation and still on page'; + +/** + * Configures the window.onbeforeunload handler such that the user will be prompted before + * reloading or navigating away + * @param options {PromptOnNavigateOptions} + * @returns {() => void} A function that, when called, will "clean up" -- restore the prior onbeforeunload handler and cancel any pending timeouts. + */ +export function promptOnNavigate(options: PromptOnNavigateOptions = defaults): () => void { + let stillOnPageTimer: number | undefined; + + function handleBeforeUnload(ev: BeforeUnloadEvent) { + ev.preventDefault(); + ev.returnValue = ''; + + trackEvent(PROMPT_EVENT); + + const stillOnPageIntervalsInSeconds = [...options.stillOnPageIntervalsInSeconds]; + let elapsed = 0; + + function scheduleNextStillOnPagePing() { + const interval = stillOnPageIntervalsInSeconds.shift(); + if (interval === undefined) { + return; + } + + if (stillOnPageTimer) { + clearTimeout(stillOnPageTimer); + stillOnPageTimer = undefined; + } + + const offsetFromNow = interval - elapsed; + elapsed = interval; + + stillOnPageTimer = window.setTimeout(() => { + trackEvent(STILL_ON_PAGE_EVENT, { + seconds: elapsed, + }); + scheduleNextStillOnPagePing(); + }, offsetFromNow * 1000); + } + + scheduleNextStillOnPagePing(); + } + + const prevHandler = window.onbeforeunload; + window.onbeforeunload = handleBeforeUnload; + + return () => { + if (window.onbeforeunload === handleBeforeUnload) { + window.onbeforeunload = prevHandler; + } + if (stillOnPageTimer) { + window.clearTimeout(stillOnPageTimer); + stillOnPageTimer = undefined; + } + }; +} diff --git a/app/javascript/packages/prompt-on-navigate/package.json b/app/javascript/packages/prompt-on-navigate/package.json new file mode 100644 index 00000000000..eeef56c2de5 --- /dev/null +++ b/app/javascript/packages/prompt-on-navigate/package.json @@ -0,0 +1,5 @@ +{ + "name": "@18f/identity-prompt-on-navigate", + "version": "1.0.0", + "private": true +} diff --git a/app/javascript/packages/test-helpers/use-sandbox.ts b/app/javascript/packages/test-helpers/use-sandbox.ts index 8899a8cac84..1c3c68f9677 100644 --- a/app/javascript/packages/test-helpers/use-sandbox.ts +++ b/app/javascript/packages/test-helpers/use-sandbox.ts @@ -22,12 +22,32 @@ function useSandbox(config?: Partial) { sandbox.clock.restore(); } + // useFakeTimers overrides global.setTimeout, etc. (callable as setTimeout()), but does not + // override window.setTimeout. So we'll do that. + const originalWindowMethods = ( + [ + 'clearImmediate', + 'clearInterval', + 'clearTimeout', + 'setImmediate', + 'setInterval', + 'setTimeout', + ] as const + ).reduce((methods, method) => { + methods[method] = window[method]; + return methods; + }, {}); + beforeEach(() => { // useFakeTimers overrides global timer functions as soon as sandbox is created, thus leaking // across tests. Instead, wait until tests start to initialize. if (useFakeTimers) { Object.assign(clockImpl, sandbox.useFakeTimers()); } + + Object.keys(originalWindowMethods).forEach((method) => { + window[method] = global[method]; + }); }); afterEach(() => { @@ -36,6 +56,10 @@ function useSandbox(config?: Partial) { if (useFakeTimers) { sandbox.clock.restore(); + + Object.keys(originalWindowMethods).forEach((method) => { + window[method] = originalWindowMethods[method]; + }); } }); diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 4de7b56937e..9f6bd0551c9 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -3581,6 +3581,25 @@ def user_marked_authed(authentication_type:, **extra) ) end + # User was shown an "Are you sure you want to navigate away from this page?" message from their + # browser (via onbeforeunload). (This is a frontend event.) + def user_prompted_before_navigation + track_event( + 'User prompted before navigation', + ) + end + + # User was shown an "Are you sure you want to navigate away from this page?" prompt via + # onbeforeunload and was still on the page later. (This is a frontend event.) + # @param [Integer] seconds Amount of time user has been on page since prompt. + def user_prompted_before_navigation_and_still_on_page(seconds:, **extra) + track_event( + 'User prompted before navigation and still on page', + seconds: seconds, + **extra, + ) + end + # @param [Boolean] success # @param [Hash] errors # @param [String] sign_up_mfa_priority_bucket diff --git a/spec/javascript/packages/document-capture-polling/index-spec.js b/spec/javascript/packages/document-capture-polling/index-spec.js index 598480cf85b..8cd21f90e78 100644 --- a/spec/javascript/packages/document-capture-polling/index-spec.js +++ b/spec/javascript/packages/document-capture-polling/index-spec.js @@ -44,6 +44,10 @@ describe('DocumentCapturePolling', () => { subject.bind(); }); + afterEach(() => { + subject.bindPromptOnNavigate(false); + }); + it('hides form', () => { expect(screen.getByText('Submit').closest('.display-none')).to.be.ok(); }); From 5bfaf49b5849b1ead83bcd5eb1b69ef0d2d02be8 Mon Sep 17 00:00:00 2001 From: Osman Latif <109746710+olatifflexion@users.noreply.github.com> Date: Fri, 2 Jun 2023 14:21:00 -0500 Subject: [PATCH 12/20] LG-9924 Add DB columns for User suspension (#8530) * LG-9924 Add DB columns for User suspension [skip changelog] --- .../20230601195606_add_columns_for_user_suspension.rb | 6 ++++++ db/schema.rb | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 db/primary_migrate/20230601195606_add_columns_for_user_suspension.rb diff --git a/db/primary_migrate/20230601195606_add_columns_for_user_suspension.rb b/db/primary_migrate/20230601195606_add_columns_for_user_suspension.rb new file mode 100644 index 00000000000..8e687d37a07 --- /dev/null +++ b/db/primary_migrate/20230601195606_add_columns_for_user_suspension.rb @@ -0,0 +1,6 @@ +class AddColumnsForUserSuspension < ActiveRecord::Migration[7.0] + def change + add_column :users, :suspended_at, :datetime + add_column :users, :reinstated_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 0135afe0ba2..0443b612784 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_05_03_231037) do +ActiveRecord::Schema[7.0].define(version: 2023_06_01_195606) do # These are extensions that must be enabled in order to support this database enable_extension "pg_stat_statements" enable_extension "pgcrypto" @@ -590,6 +590,8 @@ t.string "email_language", limit: 10 t.datetime "accepted_terms_at", precision: nil t.datetime "encrypted_recovery_code_digest_generated_at", precision: nil + t.datetime "suspended_at" + t.datetime "reinstated_at" t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["uuid"], name: "index_users_on_uuid", unique: true end From 88206b236d9cdc665b95d4e42f877f7f4a0dfe1b Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 5 Jun 2023 08:25:22 -0400 Subject: [PATCH 13/20] LG-9872: Add missing interpolation value for personal key alert (#8531) * LG-9872: Add missing interpolation value for personal key alert changelog: Bug Fixes, Sign-In Alerts, Fix message for personal key MFA sign-in missing "app_name" value * Handle valid exceptions for missing interpolations * Update spec reference to string key Caught by the new spec helper :raised_hands: --- lib/telephony/alert_sender.rb | 2 +- .../sign_in_via_personal_key_spec.rb | 7 ++++--- spec/lib/telephony/alert_sender_spec.rb | 4 +++- spec/models/event_spec.rb | 7 +++++-- spec/support/i18n_helper.rb | 13 +++++++++++++ 5 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 spec/support/i18n_helper.rb diff --git a/lib/telephony/alert_sender.rb b/lib/telephony/alert_sender.rb index 7686fc248f3..a8e08bd2983 100644 --- a/lib/telephony/alert_sender.rb +++ b/lib/telephony/alert_sender.rb @@ -40,7 +40,7 @@ def send_personal_key_regeneration_notice(to:, country_code:) end def send_personal_key_sign_in_notice(to:, country_code:) - message = I18n.t('telephony.personal_key_sign_in_notice') + message = I18n.t('telephony.personal_key_sign_in_notice', app_name: APP_NAME) response = adapter.send(message: message, to: to, country_code: country_code) log_response(response, context: __method__.to_s.gsub(/^send_/, '')) response diff --git a/spec/features/two_factor_authentication/sign_in_via_personal_key_spec.rb b/spec/features/two_factor_authentication/sign_in_via_personal_key_spec.rb index e7dee55b7aa..912037e2dfe 100644 --- a/spec/features/two_factor_authentication/sign_in_via_personal_key_spec.rb +++ b/spec/features/two_factor_authentication/sign_in_via_personal_key_spec.rb @@ -9,9 +9,6 @@ raw_key = PersonalKeyGenerator.new(user).create old_key = user.reload.encrypted_recovery_code_digest - expect(Telephony).to receive(:send_personal_key_sign_in_notice). - with(to: '+1 (202) 345-6789', country_code: 'US') - sign_in_before_2fa(user) choose_another_security_option('personal_key') enter_personal_key(personal_key: raw_key) @@ -19,6 +16,10 @@ expect(user.reload.encrypted_recovery_code_digest).to_not eq old_key expect(current_path).to eq account_path + last_message = Telephony::Test::Message.messages.last + expect(last_message.body).to eq t('telephony.personal_key_sign_in_notice', app_name: APP_NAME) + expect(last_message.to).to eq user.phone_configurations.take.phone + expect_delivered_email_count(1) expect_delivered_email( to: [user.email_addresses.first.email], diff --git a/spec/lib/telephony/alert_sender_spec.rb b/spec/lib/telephony/alert_sender_spec.rb index 1484886511b..a206a54aeff 100644 --- a/spec/lib/telephony/alert_sender_spec.rb +++ b/spec/lib/telephony/alert_sender_spec.rb @@ -117,7 +117,9 @@ last_message = Telephony::Test::Message.messages.last expect(last_message.to).to eq(recipient) - expect(last_message.body).to eq(I18n.t('telephony.personal_key_sign_in_notice')) + expect(last_message.body).to eq( + t('telephony.personal_key_sign_in_notice', app_name: APP_NAME), + ) end end end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index e451630f1c5..8096d825a83 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -15,8 +15,11 @@ it 'has a translation for every event type' do missing_translations = Event.event_types.keys.select do |event_type| - translation = I18n.t("event_types.#{event_type}", raise: true) - translation.empty? + I18n.t( + "event_types.#{event_type}", + raise: true, + ignore_test_helper_missing_interpolation: true, + ).empty? rescue I18n::MissingTranslationData true end diff --git a/spec/support/i18n_helper.rb b/spec/support/i18n_helper.rb new file mode 100644 index 00000000000..139019a3084 --- /dev/null +++ b/spec/support/i18n_helper.rb @@ -0,0 +1,13 @@ +module I18n + class << self + prepend( + Module.new do + def t(*args, ignore_test_helper_missing_interpolation: false, **kwargs) + result = super(*args, **kwargs) + return result if ignore_test_helper_missing_interpolation || !result.include?('%{') + raise "Missing interpolation in translated string: #{result}" + end + end, + ) + end +end From 92cde7558f2b424b0e2f237861ac788853c0a0a3 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 5 Jun 2023 10:01:12 -0400 Subject: [PATCH 14/20] LG-9974: Remove verified info accordion from reactivation (#8532) * LG-9974: Remove verified info accordion from reactivation changelog: User-Facing Improvements, Account Reactivation, Update content on password reentry step * Remove more unused * Update reference to decrypted_pii --- app/assets/stylesheets/components/_icon.scss | 19 --------- .../users/verify_password_controller.rb | 14 +------ app/views/shared/_pii_review.html.erb | 42 ------------------- app/views/users/verify_password/new.html.erb | 7 ---- config/locales/idv/en.yml | 7 ---- config/locales/idv/es.yml | 7 ---- config/locales/idv/fr.yml | 7 ---- .../users/verify_password_controller_spec.rb | 2 - 8 files changed, 2 insertions(+), 103 deletions(-) delete mode 100644 app/views/shared/_pii_review.html.erb diff --git a/app/assets/stylesheets/components/_icon.scss b/app/assets/stylesheets/components/_icon.scss index 708d8447270..661fd037fb0 100644 --- a/app/assets/stylesheets/components/_icon.scss +++ b/app/assets/stylesheets/components/_icon.scss @@ -20,22 +20,3 @@ $icon-min-padding: 2px; margin-right: #{0.5rem - px-to-rem($icon-min-padding)}; } } - -.ico-absolute { - background-repeat: no-repeat; - background-size: $h4; - position: relative; - - &-success { - &::before { - background-image: url('/alert/success.svg'); - content: ''; - display: block; - height: $h4; - left: units(neg-4); - position: absolute; - top: (lh('body', $theme-body-line-height) - $h4) * 0.5; - width: $h4; - } - } -} diff --git a/app/controllers/users/verify_password_controller.rb b/app/controllers/users/verify_password_controller.rb index 2e656413473..fd006b6a8f7 100644 --- a/app/controllers/users/verify_password_controller.rb +++ b/app/controllers/users/verify_password_controller.rb @@ -6,12 +6,9 @@ class VerifyPasswordController < ApplicationController before_action :confirm_password_reset_profile before_action :confirm_personal_key - def new - @decrypted_pii = decrypted_pii - end + def new; end def update - @decrypted_pii = decrypted_pii result = verify_password_form.submit irs_attempts_api_tracker.logged_in_profile_change_reauthentication_submitted( @@ -32,13 +29,6 @@ def confirm_personal_key redirect_to root_url end - # rubocop:disable Naming/MemoizedInstanceVariableName - # @return [Pii::Attributes, nil] - def decrypted_pii - @_decrypted_pii ||= reactivate_account_session.decrypted_pii - end - # rubocop:enable Naming/MemoizedInstanceVariableName - def handle_success(result) flash[:personal_key] = result.extra[:personal_key] irs_attempts_api_tracker.idv_personal_key_generated @@ -50,7 +40,7 @@ def verify_password_form VerifyPasswordForm.new( user: current_user, password: params.require(:user).permit(:password)[:password], - decrypted_pii: decrypted_pii, + decrypted_pii: reactivate_account_session.decrypted_pii, ) end end diff --git a/app/views/shared/_pii_review.html.erb b/app/views/shared/_pii_review.html.erb deleted file mode 100644 index 37c5c82f5a5..00000000000 --- a/app/views/shared/_pii_review.html.erb +++ /dev/null @@ -1,42 +0,0 @@ -
-
- <%= t('idv.review.full_name') %> -
- -
- <%= pii[:first_name] %> <%= pii[:last_name] %> -
- -
- <%= t('idv.review.mailing_address') %> -
- -
- <%= render 'shared/address', address: pii %> -
- -
- <%= t('idv.review.dob') %> -
- -
- <%= DateParser.parse_legacy(pii[:dob]).to_formatted_s(:long) %> -
- -
- <%= t('idv.review.ssn') %> -
- -
- <%= pii[:ssn] %> -
- - <% if phone %> -
- <%= t('idv.messages.phone.phone_of_record') %> -
-
- <%= PhoneFormatter.format(phone) %> -
- <% end %> -
diff --git a/app/views/users/verify_password/new.html.erb b/app/views/users/verify_password/new.html.erb index 4062087225e..a05a0abd77f 100644 --- a/app/views/users/verify_password/new.html.erb +++ b/app/views/users/verify_password/new.html.erb @@ -23,10 +23,3 @@ ) %> <%= f.submit t('forms.buttons.continue'), class: 'margin-top-5' %> <% end %> - -
- <%= render AccordionComponent.new do |c| %> - <% c.with_header { t('idv.messages.review.intro') } %> - <%= render 'shared/pii_review', pii: @decrypted_pii, phone: @decrypted_pii[:phone] %> - <% end %> -
diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml index ca19f31e4de..23976eacca7 100644 --- a/config/locales/idv/en.yml +++ b/config/locales/idv/en.yml @@ -213,14 +213,12 @@ en: alert_html: 'Enter a phone number that is:' description: We’ll check this number with records and send you a one-time code. This is to help verify your identity. - phone_of_record: Phone of record rules: - Based in the United States (including U.S. territories) - Your primary number (the one you use the most often) return_to_profile: '‹ Return to your %{app_name} profile' review: gpo_pending: We’ll send your letter once you re-enter your password. - intro: Your verified information message: '%{app_name} will encrypt your information with your password. This means that your information is secure and only you will be able to access or change it.' @@ -235,11 +233,6 @@ en: review_message: When you re-enter your password, %{app_name} will protect the information you’ve given us, so that only you can access it. verifying: Verifying… - review: - dob: Date of birth - full_name: Full name - mailing_address: Mailing address - ssn: Social Security number (SSN) titles: activated: Your identity has already been verified come_back_later: Your letter is on the way diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml index f2368f24d79..8de261b44d7 100644 --- a/config/locales/idv/es.yml +++ b/config/locales/idv/es.yml @@ -228,14 +228,12 @@ es: alert_html: 'Introduzca un número de teléfono que sea:' description: Comprobaremos este número con los registros y le enviaremos un código único. Esto es para ayudar a verificar su identidad. - phone_of_record: Teléfono del registro rules: - Con base en Estados Unidos (incluidos los territorios de EE.UU.) - Su número principal (el que utiliza con más frecuencia) return_to_profile: '‹ Volver a tu perfil de %{app_name}' review: gpo_pending: Enviaremos tu carta una vez que hayas ingresado tu contraseña. - intro: Su información verificada message: '%{app_name} encriptará tu información con tu contraseña. Esto significa que tu información estará segura y solo tú podrás consultarla o modificarla.' @@ -251,11 +249,6 @@ es: review_message: Cuando vuelva a ingresar su contraseña, %{app_name} cifrará sus datos para asegurarse de que nadie más pueda acceder a ellos. verifying: Verificando… - review: - dob: Fecha de nacimiento - full_name: Nombre completo - mailing_address: Dirección postal - ssn: Número de Seguro Social (SSN, sigla en inglés) titles: activated: Ya se verificó tu identidad. come_back_later: Su carta está en camino diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml index 5c7316cb18c..32ee3c3b972 100644 --- a/config/locales/idv/fr.yml +++ b/config/locales/idv/fr.yml @@ -243,7 +243,6 @@ fr: alert_html: 'Entrez un numéro de téléphone qui est :' description: Nous vérifierons ce numéro dans nos archives et vous enverrons un code à usage unique. Ceci est pour aider à vérifier votre identité. - phone_of_record: numéro de téléphone enregistré rules: - Basé aux Etats-Unis (y compris les territoires américains) - Votre numéro principal (celui que vous utilisez le plus souvent) @@ -251,7 +250,6 @@ fr: review: gpo_pending: Nous vous enverrons votre lettre une fois que vous aurez réintroduit votre mot de passe. - intro: Vos informations vérifiées message: '%{app_name} crypte vos informations avec votre mot de passe. Cela signifie que vos informations sont sécurisées et que vous seul pourrez y accéder ou les modifier.' @@ -268,11 +266,6 @@ fr: review_message: Lorsque vous entrez à nouveau votre mot de passe, %{app_name} crypte vos données pour vous assurer que personne ne peut y accéder. verifying: Vérification… - review: - dob: Date de naissance - full_name: Nom complet - mailing_address: Adresse postale - ssn: Numéro de sécurité sociale (SSN) titles: activated: Votre identité a déjà été vérifiée come_back_later: Votre lettre est en route diff --git a/spec/controllers/users/verify_password_controller_spec.rb b/spec/controllers/users/verify_password_controller_spec.rb index 34c1db19235..55604aa76a5 100644 --- a/spec/controllers/users/verify_password_controller_spec.rb +++ b/spec/controllers/users/verify_password_controller_spec.rb @@ -91,14 +91,12 @@ end context 'without valid password' do - let(:pii) { { dob: Time.zone.today } } let(:response_bad) { FormResponse.new(success: false, errors: {}) } render_views before do allow(form).to receive(:submit).and_return(response_bad) - allow(controller).to receive(:decrypted_pii).and_return(pii) put :update, params: user_params end From 9c60fa9becff5ed25ddb513b19e774b8183f296d Mon Sep 17 00:00:00 2001 From: Malick Diarra Date: Mon, 5 Jun 2023 10:04:32 -0400 Subject: [PATCH 15/20] LG-9836: Show Recaptcha cancel when adding phone (#8458) * changelog: Upcoming Features, Recaptcha, Add cancel option in spam protection screen * check phone controller, and add cancel option for when going through adding phone to curent account * add user * check rspec * rubocop fix * local assigns * remove unneeded check * fix space * remove unneeded stubs --- .../phone_setup/spam_protection.html.erb | 6 +++++ .../spam_protection.html.erb_spec.rb | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/app/views/users/phone_setup/spam_protection.html.erb b/app/views/users/phone_setup/spam_protection.html.erb index 61d823d06f7..93147fbf77a 100644 --- a/app/views/users/phone_setup/spam_protection.html.erb +++ b/app/views/users/phone_setup/spam_protection.html.erb @@ -58,3 +58,9 @@ new_tab: true, ).with_content(t('two_factor_authentication.phone_verification.troubleshooting.learn_more')) %> <% end %> + +<% unless local_assigns[:two_factor_options_path].present? %> + <%= render PageFooterComponent.new do %> + <%= link_to t('links.cancel'), account_path %> + <% end %> +<% end %> \ No newline at end of file diff --git a/spec/views/phone_setup/spam_protection.html.erb_spec.rb b/spec/views/phone_setup/spam_protection.html.erb_spec.rb index f79897e0e1b..8d5ef4a66c1 100644 --- a/spec/views/phone_setup/spam_protection.html.erb_spec.rb +++ b/spec/views/phone_setup/spam_protection.html.erb_spec.rb @@ -43,6 +43,31 @@ href: two_factor_options_path, ) end + + it 'does not render cancel option' do + expect(rendered).to_not have_link( + t('links.cancel'), + href: account_path, + ) + end + end + + context 'fully registered user adding new phone' do + let(:user) { create(:user, :fully_registered) } + + it 'does not render additional troubleshooting option to two factor options' do + expect(rendered).to_not have_link( + t('two_factor_authentication.login_options_link_text'), + href: two_factor_options_path, + ) + end + + it 'renders cancel option' do + expect(rendered).to have_link( + t('links.cancel'), + href: account_path, + ) + end end context 'with configured recaptcha site key' do From a3761656177f7748047ff11593f873be00d8013e Mon Sep 17 00:00:00 2001 From: jc-gsa <104452882+jc-gsa@users.noreply.github.com> Date: Mon, 5 Jun 2023 09:47:56 -0500 Subject: [PATCH 16/20] LG-9714 - Remove feature flag for password confirmation (#8526) * Remove flags for password confirmation changelog: Internal, Registration, Remove feature flag related to password confirmation --- .../password_confirmation_component.html.erb | 2 - .../sign_up/passwords_controller.rb | 3 +- app/forms/password_form.rb | 1 - app/validators/form_password_validator.rb | 6 +- .../sign_up/passwords_controller_spec.rb | 134 ++++++++---------- spec/forms/password_form_spec.rb | 1 - 6 files changed, 67 insertions(+), 80 deletions(-) diff --git a/app/components/password_confirmation_component.html.erb b/app/components/password_confirmation_component.html.erb index d3d52b0d219..683da5ccd90 100644 --- a/app/components/password_confirmation_component.html.erb +++ b/app/components/password_confirmation_component.html.erb @@ -43,6 +43,4 @@ > <%= t('components.password_confirmation.toggle_label') %> - - <%= form.hidden_field :confirmation_enabled, value: true %> <% end %> diff --git a/app/controllers/sign_up/passwords_controller.rb b/app/controllers/sign_up/passwords_controller.rb index 7dd4076a806..bfb209a862b 100644 --- a/app/controllers/sign_up/passwords_controller.rb +++ b/app/controllers/sign_up/passwords_controller.rb @@ -50,8 +50,7 @@ def track_analytics(result) def permitted_params params.require(:password_form).permit( - :confirmation_token, :password, :password_confirmation, - :confirmation_enabled + :confirmation_token, :password, :password_confirmation ) end diff --git a/app/forms/password_form.rb b/app/forms/password_form.rb index 3aebe12ff63..d0820c8d52d 100644 --- a/app/forms/password_form.rb +++ b/app/forms/password_form.rb @@ -11,7 +11,6 @@ def submit(params) @password = params[:password] @password_confirmation = params[:password_confirmation] @request_id = params.fetch(:request_id, '') - @confirmation_enabled = params[:confirmation_enabled].presence FormResponse.new(success: valid?, errors: errors, extra: extra_analytics_attributes) end diff --git a/app/validators/form_password_validator.rb b/app/validators/form_password_validator.rb index 4e363d3c1d2..2f83c7e86b6 100644 --- a/app/validators/form_password_validator.rb +++ b/app/validators/form_password_validator.rb @@ -2,7 +2,7 @@ module FormPasswordValidator extend ActiveSupport::Concern included do - attr_accessor :password, :password_confirmation, :validate_confirmation, :confirmation_enabled + attr_accessor :password, :password_confirmation, :validate_confirmation attr_reader :user validates :password, @@ -11,7 +11,7 @@ module FormPasswordValidator validates :password_confirmation, presence: true, length: { in: Devise.password_length }, - if: -> { confirmation_enabled && validate_confirmation } + if: -> { validate_confirmation } validate :password_graphemes_length, :strong_password, :not_pwned, :passwords_match end @@ -54,7 +54,7 @@ def not_pwned end def passwords_match - return unless confirmation_enabled && validate_confirmation + return unless validate_confirmation if password != password_confirmation errors.add( diff --git a/spec/controllers/sign_up/passwords_controller_spec.rb b/spec/controllers/sign_up/passwords_controller_spec.rb index bda5a03d946..8dcf444c235 100644 --- a/spec/controllers/sign_up/passwords_controller_spec.rb +++ b/spec/controllers/sign_up/passwords_controller_spec.rb @@ -10,7 +10,6 @@ password_form: { password: password, password_confirmation: password_confirmation, - confirmation_enabled: true, }, confirmation_token: token, } @@ -77,80 +76,73 @@ stub_attempts_tracker end - context 'with temporary param confirmation_enabled' do - before do - params.merge(confirmation_enabled: true) + context 'with a password that is too short' do + let(:password) { 'NewVal' } + let(:password_confirmation) { 'NewVal' } + let(:errors) do + { + password: + [t( + 'errors.attributes.password.too_short.other', + count: Devise.password_length.first, + )], + password_confirmation: + ["is too short (minimum is #{Devise.password_length.first} characters)"], + } end + let(:error_details) do + { + password: [:too_short], + password_confirmation: [:too_short], + } + end + + it 'tracks an invalid password event' do + expect(@analytics).to receive(:track_event). + with( + 'User Registration: Email Confirmation', + { errors: {}, error_details: nil, success: true, user_id: user.uuid }, + ) + expect(@analytics).to receive(:track_event). + with('Password Creation', analytics_hash) + + expect(@irs_attempts_api_tracker).to receive(:user_registration_password_submitted). + with( + success: false, + failure_reason: error_details, + ) + expect(@irs_attempts_api_tracker).not_to receive(:user_registration_email_confirmation) + + subject + end + end - # modify this test - context 'with a password that is too short' do - let(:password) { 'NewVal' } - let(:password_confirmation) { 'NewVal' } - let(:errors) do - { - password: - [t( - 'errors.attributes.password.too_short.other', - count: Devise.password_length.first, - )], - password_confirmation: - ["is too short (minimum is #{Devise.password_length.first} characters)"], - } - end - let(:error_details) do - { - password: [:too_short], - password_confirmation: [:too_short], - } - end - - it 'tracks an invalid password event' do - expect(@analytics).to receive(:track_event). - with( - 'User Registration: Email Confirmation', - { errors: {}, error_details: nil, success: true, user_id: user.uuid }, - ) - expect(@analytics).to receive(:track_event). - with('Password Creation', analytics_hash) - - expect(@irs_attempts_api_tracker).to receive(:user_registration_password_submitted). - with( - success: false, - failure_reason: error_details, - ) - expect(@irs_attempts_api_tracker).not_to receive(:user_registration_email_confirmation) - - subject - end + context 'when password confirmation does not match' do + let(:password) { 'NewVal!dPassw0rd' } + let(:password_confirmation) { 'bad match password' } + let(:errors) do + { + password_confirmation: + [t('errors.messages.password_mismatch')], + } end + let(:error_details) do + { + password_confirmation: [t('errors.messages.password_mismatch')], + } + end + + it 'tracks invalid password_confirmation error' do + expect(@analytics).to receive(:track_event). + with( + 'User Registration: Email Confirmation', + { errors: {}, error_details: nil, success: true, user_id: user.uuid }, + ) + + expect(@analytics).to receive(:track_event). + with('Password Creation', analytics_hash) - context 'when password confirmation does not match' do - let(:password) { 'NewVal!dPassw0rd' } - let(:password_confirmation) { 'bad match password' } - let(:errors) do - { - password_confirmation: - [t('errors.messages.password_mismatch')], - } - end - let(:error_details) do - { - password_confirmation: [t('errors.messages.password_mismatch')], - } - end - - it 'tracks invalid password_confirmation error' do - expect(@analytics).to receive(:track_event). - with( - 'User Registration: Email Confirmation', - { errors: {}, error_details: nil, success: true, user_id: user.uuid }, - ) - - expect(@analytics).to receive(:track_event). - with('Password Creation', analytics_hash) - - subject - end + subject end end end diff --git a/spec/forms/password_form_spec.rb b/spec/forms/password_form_spec.rb index ff96adccd19..b1ec0abe7a6 100644 --- a/spec/forms/password_form_spec.rb +++ b/spec/forms/password_form_spec.rb @@ -21,7 +21,6 @@ { password: password, password_confirmation: password_confirmation, - confirmation_enabled: true, } end From b73775d97aee1c7c92337fca003a45ad8bcf9495 Mon Sep 17 00:00:00 2001 From: Jack Ryan Date: Mon, 5 Jun 2023 16:59:10 -0400 Subject: [PATCH 17/20] LG 8441 Prioritize ready enrollments (#8488) * LG-8440: Install aws-sdk-ruby; create migration for status check field * initial class creation and job configuration making * thinking comments * subclassing done * Adding appropriate subclass and tests for new methods for in person enrollment * add second job * Trim down specs * Use helper methods for config checks * Create new helper modules * Refactor to use helper modules * Undoing yarn bomb * undoing mystery changes to packages * changelog: Upcoming Features, In-person Proofing, Add two jobs to check enrollments that are ready * correct class name in job config and whack with linter * appeasing linter gods * proper documentation and not nil for job name * moving not nil values up * Cleaning up application * cleaning up analytics events * chaining methods instead of rewriting queries * Updating analytics to remove alteration of job name and instead as a field * update error comment * linty lint lint * Refactor modules to classes * Abandon module refactor * Linty lint lint * Added in check for in_person_enrollments_ready_job_enabled * lint fix * lint and decimal fix --------- Co-authored-by: Timothy Bradley --- app/jobs/get_usps_proofing_results_job.rb | 180 +++++------ .../get_usps_ready_proofing_results_job.rb | 40 +++ .../get_usps_waiting_proofing_results_job.rb | 40 +++ app/models/in_person_enrollment.rb | 20 ++ config/application.yml.default | 4 +- config/initializers/job_configurations.rb | 12 + lib/identity_config.rb | 2 + .../get_usps_proofing_results_job_spec.rb | 55 +++- ...et_usps_ready_proofing_results_job_spec.rb | 287 ++++++++++++++++++ ..._usps_waiting_proofing_results_job_spec.rb | 287 ++++++++++++++++++ spec/models/in_person_enrollment_spec.rb | 109 +++++++ 11 files changed, 943 insertions(+), 93 deletions(-) create mode 100644 app/jobs/get_usps_ready_proofing_results_job.rb create mode 100644 app/jobs/get_usps_waiting_proofing_results_job.rb create mode 100644 spec/jobs/get_usps_ready_proofing_results_job_spec.rb create mode 100644 spec/jobs/get_usps_waiting_proofing_results_job_spec.rb diff --git a/app/jobs/get_usps_proofing_results_job.rb b/app/jobs/get_usps_proofing_results_job.rb index 669677b7ad4..b0a1a0b59ac 100644 --- a/app/jobs/get_usps_proofing_results_job.rb +++ b/app/jobs/get_usps_proofing_results_job.rb @@ -14,7 +14,8 @@ class GetUspsProofingResultsJob < ApplicationJob queue_as :long_running def perform(_now) - return true unless IdentityConfig.store.in_person_proofing_enabled + return true unless ipp_enabled? + return true if ipp_ready_job_enabled? @enrollment_outcomes = { enrollments_checked: 0, @@ -35,23 +36,16 @@ def perform(_now) analytics.idv_in_person_usps_proofing_results_job_started( enrollments_count: enrollments.count, reprocess_delay_minutes: reprocess_delay_minutes, + job_name: self.class.name, ) check_enrollments(enrollments) - percent_enrollments_errored = 0 - if enrollment_outcomes[:enrollments_checked] > 0 - percent_enrollments_errored = - (enrollment_outcomes[:enrollments_errored].fdiv( - enrollment_outcomes[:enrollments_checked], - ) * 100).round(2) - end - analytics.idv_in_person_usps_proofing_results_job_completed( **enrollment_outcomes, duration_seconds: (Time.zone.now - started_at).seconds.round(2), - # Calculate % of errored enrollments - percent_enrollments_errored:, + percent_enrollments_errored: percent_errored, + job_name: self.class.name, ) true @@ -69,6 +63,14 @@ def proofer @proofer ||= UspsInPersonProofing::Proofer.new end + def ipp_enabled? + IdentityConfig.store.in_person_proofing_enabled == true + end + + def ipp_ready_job_enabled? + IdentityConfig.store.in_person_enrollments_ready_job_enabled == true + end + def check_enrollments(enrollments) last_enrollment_index = enrollments.length - 1 enrollments.each_with_index do |enrollment, idx| @@ -92,14 +94,10 @@ def check_enrollment(enrollment) rescue Faraday::BadRequestError => err # 400 status code. This is used for some status updates and some common client errors handle_bad_request_error(err, enrollment) - rescue Faraday::ClientError, Faraday::ServerError => err - # 4xx or 5xx status code. These are unexpected but will have some sort of - # response body that we can try to log data from + rescue Faraday::ClientError, Faraday::ServerError, Faraday::Error => err + # 4xx, 5xx and any other Faraday error besides a 400 status code. + # These errors may or may not have a response body that we can pull info from. handle_client_or_server_error(err, enrollment) - rescue Faraday::Error => err - # Timeouts, failed connections, parsing errors, and other HTTP errors. These - # generally won't have a response body - handle_faraday_error(err, enrollment) rescue StandardError => err handle_standard_error(err, enrollment) else @@ -113,48 +111,15 @@ def analytics(user: AnonymousUser.new) Analytics.new(user: user, request: nil, session: {}, sp: nil) end - def email_analytics_attributes(enrollment) - { - enrollment_code: enrollment.enrollment_code, - timestamp: Time.zone.now, - service_provider: enrollment.issuer, - wait_until: mail_delivery_params(enrollment.proofed_at)[:wait_until], - } - end - - def enrollment_analytics_attributes(enrollment, complete:) - { - enrollment_code: enrollment.enrollment_code, - enrollment_id: enrollment.id, - minutes_since_last_status_check: enrollment.minutes_since_last_status_check, - minutes_since_last_status_check_completed: - enrollment.minutes_since_last_status_check_completed, - minutes_since_last_status_update: enrollment.minutes_since_last_status_update, - minutes_since_established: enrollment.minutes_since_established, - minutes_to_completion: complete ? enrollment.minutes_since_established : nil, - issuer: enrollment.issuer, - } - end - - def response_analytics_attributes(response) - return { response_present: false } unless response.present? - - { - fraud_suspected: response['fraudSuspected'], - primary_id_type: response['primaryIdType'], - secondary_id_type: response['secondaryIdType'], - failure_reason: response['failureReason'], - transaction_end_date_time: parse_usps_timestamp(response['transactionEndDateTime']), - transaction_start_date_time: parse_usps_timestamp(response['transactionStartDateTime']), - status: response['status'], - assurance_level: response['assuranceLevel'], - proofing_post_office: response['proofingPostOffice'], - proofing_city: response['proofingCity'], - proofing_state: response['proofingState'], - scan_count: response['scanCount'], - response_message: response['responseMessage'], - response_present: true, - } + def percent_errored + error_rate = 0 + if enrollment_outcomes[:enrollments_checked] > 0 + error_rate = + (enrollment_outcomes[:enrollments_errored].fdiv( + enrollment_outcomes[:enrollments_checked], + ) * 100).round(2) + end + error_rate end def handle_bad_request_error(err, enrollment) @@ -178,16 +143,7 @@ def handle_bad_request_error(err, enrollment) enrollment, response_message, reason: 'Invalid applicant unique id' ) else - NewRelic::Agent.notice_error(err) - analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_exception( - **enrollment_analytics_attributes(enrollment, complete: false), - **response_analytics_attributes(response_body), - exception_class: err.class.to_s, - exception_message: err.message, - reason: 'Request exception', - response_status_code: err.response_status, - ) - enrollment_outcomes[:enrollments_errored] += 1 + handle_client_or_server_error(err, enrollment) end end @@ -200,21 +156,7 @@ def handle_client_or_server_error(err, enrollment) exception_message: err.message, reason: 'Request exception', response_status_code: err.response_status, - ) - enrollment_outcomes[:enrollments_errored] += 1 - end - - def handle_faraday_error(err, enrollment) - NewRelic::Agent.notice_error(err) - analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_exception( - **enrollment_analytics_attributes(enrollment, complete: false), - # There probably isn't a response body or status for these types of errors but we try to log - # them in case there is - **response_analytics_attributes(err.response_body), - response_status_code: err.response_status, - exception_class: err.class.to_s, - exception_message: err.message, - reason: 'Request exception', + job_name: self.class.name, ) enrollment_outcomes[:enrollments_errored] += 1 end @@ -222,32 +164,35 @@ def handle_faraday_error(err, enrollment) def handle_standard_error(err, enrollment) NewRelic::Agent.notice_error(err) response_attributes = response_analytics_attributes(nil) - enrollment_outcomes[:enrollments_errored] += 1 analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_exception( **enrollment_analytics_attributes(enrollment, complete: false), **response_attributes, exception_class: err.class.to_s, exception_message: err.message, reason: 'Request exception', + job_name: self.class.name, ) + enrollment_outcomes[:enrollments_errored] += 1 end def handle_response_is_not_a_hash(enrollment) - enrollment_outcomes[:enrollments_errored] += 1 analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_exception( **enrollment_analytics_attributes(enrollment, complete: false), reason: 'Bad response structure', + job_name: self.class.name, ) + enrollment_outcomes[:enrollments_errored] += 1 end def handle_unsupported_status(enrollment, response) - enrollment_outcomes[:enrollments_errored] += 1 analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_exception( **enrollment_analytics_attributes(enrollment, complete: false), **response_analytics_attributes(response), reason: 'Unsupported status', status: response['status'], + job_name: self.class.name, ) + enrollment_outcomes[:enrollments_errored] += 1 end def handle_unsupported_id_type(enrollment, response) @@ -259,6 +204,7 @@ def handle_unsupported_id_type(enrollment, response) passed: false, primary_id_type: response['primaryIdType'], reason: 'Unsupported ID type', + job_name: self.class.name, ) enrollment.update( status: :failed, @@ -270,6 +216,7 @@ def handle_unsupported_id_type(enrollment, response) analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated( **email_analytics_attributes(enrollment), email_type: 'Failed unsupported ID type', + job_name: self.class.name, ) end @@ -279,8 +226,10 @@ def handle_incomplete_status_update(enrollment, response_message) idv_in_person_usps_proofing_results_job_enrollment_incomplete( **enrollment_analytics_attributes(enrollment, complete: false), response_message: response_message, + job_name: self.class.name, ) enrollment.update(status_check_completed_at: Time.zone.now) + enrollment.update(status_check_completed_at: Time.zone.now) end def handle_expired_status_update(enrollment, response, response_message) @@ -290,6 +239,7 @@ def handle_expired_status_update(enrollment, response, response_message) **response_analytics_attributes(response[:body]), passed: false, reason: 'Enrollment has expired', + job_name: self.class.name, ) enrollment.update( status: :expired, @@ -305,12 +255,14 @@ def handle_expired_status_update(enrollment, response, response_message) enrollment_id: enrollment.id, exception_class: err.class.to_s, exception_message: err.message, + job_name: self.class.name, ) else analytics(user: enrollment.user). idv_in_person_usps_proofing_results_job_deadline_passed_email_initiated( **email_analytics_attributes(enrollment), enrollment_id: enrollment.id, + job_name: self.class.name, ) enrollment.update(deadline_passed_sent: true) end @@ -337,6 +289,7 @@ def handle_unexpected_response(enrollment, response_message, reason:, cancel: tr **enrollment_analytics_attributes(enrollment, complete: cancel), response_message: response_message, reason: reason, + job_name: self.class.name, ) end @@ -348,6 +301,7 @@ def handle_failed_status(enrollment, response) **response_analytics_attributes(response), passed: false, reason: 'Failed status', + job_name: self.class.name, ) enrollment.update( @@ -360,12 +314,14 @@ def handle_failed_status(enrollment, response) analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated( **email_analytics_attributes(enrollment), email_type: 'Failed fraud suspected', + job_name: self.class.name, ) else send_failed_email(enrollment.user, enrollment) analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated( **email_analytics_attributes(enrollment), email_type: 'Failed', + job_name: self.class.name, ) end end @@ -378,6 +334,7 @@ def handle_successful_status_update(enrollment, response) **response_analytics_attributes(response), passed: true, reason: 'Successful status update', + job_name: self.class.name, ) enrollment.profile.activate_after_passing_in_person enrollment.update( @@ -389,6 +346,7 @@ def handle_successful_status_update(enrollment, response) analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated( **email_analytics_attributes(enrollment), email_type: 'Success', + job_name: self.class.name, ) end @@ -400,6 +358,7 @@ def handle_unsupported_secondary_id(enrollment, response) **response_analytics_attributes(response), passed: false, reason: 'Provided secondary proof of address', + job_name: self.class.name, ) enrollment.update( status: :failed, @@ -410,6 +369,7 @@ def handle_unsupported_secondary_id(enrollment, response) analytics(user: enrollment.user).idv_in_person_usps_proofing_results_job_email_initiated( **email_analytics_attributes(enrollment), email_type: 'Failed unsupported secondary ID', + job_name: self.class.name, ) end @@ -484,6 +444,50 @@ def mail_delivery_params(proofed_at) return { wait_until: wait_until, queue: :intentionally_delayed } end + def email_analytics_attributes(enrollment) + { + enrollment_code: enrollment.enrollment_code, + timestamp: Time.zone.now, + service_provider: enrollment.issuer, + wait_until: mail_delivery_params(enrollment.proofed_at)[:wait_until], + } + end + + def enrollment_analytics_attributes(enrollment, complete:) + { + enrollment_code: enrollment.enrollment_code, + enrollment_id: enrollment.id, + minutes_since_last_status_check: enrollment.minutes_since_last_status_check, + minutes_since_last_status_check_completed: + enrollment.minutes_since_last_status_check_completed, + minutes_since_last_status_update: enrollment.minutes_since_last_status_update, + minutes_since_established: enrollment.minutes_since_established, + minutes_to_completion: complete ? enrollment.minutes_since_established : nil, + issuer: enrollment.issuer, + } + end + + def response_analytics_attributes(response) + return { response_present: false } unless response.present? + + { + fraud_suspected: response['fraudSuspected'], + primary_id_type: response['primaryIdType'], + secondary_id_type: response['secondaryIdType'], + failure_reason: response['failureReason'], + transaction_end_date_time: parse_usps_timestamp(response['transactionEndDateTime']), + transaction_start_date_time: parse_usps_timestamp(response['transactionStartDateTime']), + status: response['status'], + assurance_level: response['assuranceLevel'], + proofing_post_office: response['proofingPostOffice'], + proofing_city: response['proofingCity'], + proofing_state: response['proofingState'], + scan_count: response['scanCount'], + response_message: response['responseMessage'], + response_present: true, + } + end + def parse_usps_timestamp(usps_timestamp) return unless usps_timestamp # Parse timestamps eg 12/17/2020 033855 => Thu, 17 Dec 2020 03:38:55 -0600 diff --git a/app/jobs/get_usps_ready_proofing_results_job.rb b/app/jobs/get_usps_ready_proofing_results_job.rb new file mode 100644 index 00000000000..89d63b6c293 --- /dev/null +++ b/app/jobs/get_usps_ready_proofing_results_job.rb @@ -0,0 +1,40 @@ +class GetUspsReadyProofingResultsJob < GetUspsProofingResultsJob + queue_as :long_running + + def perform(_now) + return true unless ipp_enabled? && ipp_ready_job_enabled? + + @enrollment_outcomes = { + enrollments_checked: 0, + enrollments_errored: 0, + enrollments_expired: 0, + enrollments_failed: 0, + enrollments_in_progress: 0, + enrollments_passed: 0, + } + + reprocess_delay_minutes = IdentityConfig.store. + get_usps_proofing_results_job_reprocess_delay_minutes + enrollments = InPersonEnrollment.needs_status_check_on_ready_enrollments( + ...reprocess_delay_minutes.minutes.ago, + ) + + started_at = Time.zone.now + analytics.idv_in_person_usps_proofing_results_job_started( + enrollments_count: enrollments.count, + reprocess_delay_minutes: reprocess_delay_minutes, + job_name: self.class.name, + ) + + check_enrollments(enrollments) + + analytics.idv_in_person_usps_proofing_results_job_completed( + **enrollment_outcomes, + duration_seconds: (Time.zone.now - started_at).seconds.round(2), + percent_enrollments_errored: percent_errored, + job_name: self.class.name, + ) + + true + end +end diff --git a/app/jobs/get_usps_waiting_proofing_results_job.rb b/app/jobs/get_usps_waiting_proofing_results_job.rb new file mode 100644 index 00000000000..a848e3ff540 --- /dev/null +++ b/app/jobs/get_usps_waiting_proofing_results_job.rb @@ -0,0 +1,40 @@ +class GetUspsWaitingProofingResultsJob < GetUspsProofingResultsJob + queue_as :long_running + + def perform(_now) + return true unless ipp_enabled? && ipp_ready_job_enabled? + + @enrollment_outcomes = { + enrollments_checked: 0, + enrollments_errored: 0, + enrollments_expired: 0, + enrollments_failed: 0, + enrollments_in_progress: 0, + enrollments_passed: 0, + } + + reprocess_delay_minutes = IdentityConfig.store. + get_usps_proofing_results_job_reprocess_delay_minutes + enrollments = InPersonEnrollment.needs_status_check_on_waiting_enrollments( + ...reprocess_delay_minutes.minutes.ago, + ) + + started_at = Time.zone.now + analytics.idv_in_person_usps_proofing_results_job_started( + enrollments_count: enrollments.count, + reprocess_delay_minutes: reprocess_delay_minutes, + job_name: self.class.name, + ) + + check_enrollments(enrollments) + + analytics.idv_in_person_usps_proofing_results_job_completed( + **enrollment_outcomes, + duration_seconds: (Time.zone.now - started_at).seconds.round(2), + percent_enrollments_errored: percent_errored, + job_name: self.class.name, + ) + + true + end +end diff --git a/app/models/in_person_enrollment.rb b/app/models/in_person_enrollment.rb index 41a5a2a2e26..46fb9cb6ba9 100644 --- a/app/models/in_person_enrollment.rb +++ b/app/models/in_person_enrollment.rb @@ -63,6 +63,26 @@ def needs_usps_status_check?(check_interval) ) end + # Find enrollments that are ready for a status check via the USPS API + def self.needs_status_check_on_ready_enrollments(check_interval) + needs_usps_status_check(check_interval).where(ready_for_status_check: true) + end + + # Does this ready enrollment need a status check via the USPS API? + def needs_status_check_on_ready_enrollment?(check_interval) + needs_usps_status_check?(check_interval) && ready_for_status_check? + end + + # Find waiting enrollments that need a status check via the USPS API + def self.needs_status_check_on_waiting_enrollments(check_interval) + needs_usps_status_check(check_interval).where(ready_for_status_check: false) + end + + # Does this waiting enrollment need a status check via the USPS API? + def needs_status_check_on_waiting_enrollment?(check_interval) + needs_usps_status_check?(check_interval) && !ready_for_status_check? + end + def minutes_since_established return unless enrollment_established_at.present? (Time.zone.now - enrollment_established_at).seconds.in_minutes.round(2) diff --git a/config/application.yml.default b/config/application.yml.default index a63c02d1c93..e53c475b156 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -355,7 +355,9 @@ usps_ipp_sponsor_id: '' usps_ipp_username: '' usps_mock_fallback: true usps_ipp_transliteration_enabled: false -get_usps_proofing_results_job_cron: '0/10 * * * *' +get_usps_ready_proofing_results_job_cron: '0/10 * * * *' +get_usps_waiting_proofing_results_job_cron: '0/30 * * * *' +get_usps_proofing_results_job_cron: '0/30 * * * *' get_usps_proofing_results_job_reprocess_delay_minutes: 5 get_usps_proofing_results_job_request_delay_milliseconds: 1000 voice_otp_pause_time: '0.5s' diff --git a/config/initializers/job_configurations.rb b/config/initializers/job_configurations.rb index 675bb6c44ec..b6380ab6fab 100644 --- a/config/initializers/job_configurations.rb +++ b/config/initializers/job_configurations.rb @@ -116,6 +116,18 @@ cron: IdentityConfig.store.get_usps_proofing_results_job_cron, args: -> { [Time.zone.now] }, }, + # Queue usps proofing job to GoodJob for ready enrollments + get_usps_ready_proofing_results_job: { + class: 'GetUspsReadyProofingResultsJob', + cron: IdentityConfig.store.get_usps_ready_proofing_results_job_cron, + args: -> { [Time.zone.now] }, + }, + # Queue usps proofing job to GoodJob for waiting enrollments + get_usps_waiting_proofing_results_job: { + class: 'GetUspsWaitingProofingResultsJob', + cron: IdentityConfig.store.get_usps_waiting_proofing_results_job_cron, + args: -> { [Time.zone.now] }, + }, # Queue daily in-person proofing reminder email job email_reminder_job: { class: 'InPerson::EmailReminderJob', diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 583f5888d03..ca7e8011ed9 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -450,6 +450,8 @@ def self.build_store(config_map) config.add(:usps_ipp_request_timeout, type: :integer) config.add(:usps_upload_enabled, type: :boolean) config.add(:usps_ipp_transliteration_enabled, type: :boolean) + config.add(:get_usps_ready_proofing_results_job_cron, type: :string) + config.add(:get_usps_waiting_proofing_results_job_cron, type: :string) config.add(:get_usps_proofing_results_job_cron, type: :string) config.add(:get_usps_proofing_results_job_reprocess_delay_minutes, type: :integer) config.add(:get_usps_proofing_results_job_request_delay_milliseconds, type: :integer) diff --git a/spec/jobs/get_usps_proofing_results_job_spec.rb b/spec/jobs/get_usps_proofing_results_job_spec.rb index aa24d69faf5..9e2b2779de3 100644 --- a/spec/jobs/get_usps_proofing_results_job_spec.rb +++ b/spec/jobs/get_usps_proofing_results_job_spec.rb @@ -43,6 +43,7 @@ status: response['status'], transaction_end_date_time: anything, transaction_start_date_time: anything, + job_name: 'GetUspsProofingResultsJob', ) end end @@ -58,6 +59,7 @@ wait_until: anything, service_provider: pending_enrollment.issuer, timestamp: Time.zone.now, + job_name: 'GetUspsProofingResultsJob', ) else expect(job_analytics).to have_logged_event( @@ -67,6 +69,7 @@ wait_until: anything, service_provider: pending_enrollment.issuer, timestamp: Time.zone.now, + job_name: 'GetUspsProofingResultsJob', ) end end @@ -111,6 +114,7 @@ reason: reason, response_message: response_message, response_status_code: response_status_code, + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -155,6 +159,7 @@ reason: 'Request exception', response_present: false, exception_class: error_type.to_s, + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -317,6 +322,7 @@ 'GetUspsProofingResultsJob: Job started', enrollments_count: 5, reprocess_delay_minutes: 2.0, + job_name: 'GetUspsProofingResultsJob', ) end @@ -342,7 +348,8 @@ enrollments_failed: 1, enrollments_in_progress: 1, enrollments_passed: 1, - percent_enrollments_errored: 20, + percent_enrollments_errored: 20.00, + job_name: 'GetUspsProofingResultsJob', ) expect( @@ -369,7 +376,8 @@ enrollments_failed: 0, enrollments_in_progress: 0, enrollments_passed: 5, - percent_enrollments_errored: 0, + percent_enrollments_errored: 0.00, + job_name: 'GetUspsProofingResultsJob', ) expect( @@ -397,7 +405,8 @@ enrollments_failed: 0, enrollments_in_progress: 0, enrollments_passed: 0, - percent_enrollments_errored: 0, + percent_enrollments_errored: 0.00, + job_name: 'GetUspsProofingResultsJob', ) expect( @@ -424,6 +433,7 @@ exception_message: error_message, exception_class: 'StandardError', reason: 'Request exception', + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -492,6 +502,7 @@ service_provider: pending_enrollment.issuer, timestamp: anything, wait_until: nil, + job_name: 'GetUspsProofingResultsJob', ) end end @@ -560,6 +571,7 @@ service_provider: anything, timestamp: anything, wait_until: wait_until, + job_name: 'GetUspsProofingResultsJob', ) end @@ -585,6 +597,7 @@ service_provider: anything, timestamp: anything, wait_until: wait_until, + job_name: 'GetUspsProofingResultsJob', ) end end @@ -632,6 +645,7 @@ hash_including( reason: 'Successful status update', passed: true, + job_name: 'GetUspsProofingResultsJob', ), ) expect(job_analytics).to have_logged_event( @@ -641,6 +655,7 @@ service_provider: anything, timestamp: anything, wait_until: nil, + job_name: 'GetUspsProofingResultsJob', ) end end @@ -668,6 +683,7 @@ hash_including( passed: false, reason: 'Failed status', + job_name: 'GetUspsProofingResultsJob', ), ) expect(job_analytics).to have_logged_event( @@ -677,6 +693,7 @@ service_provider: anything, timestamp: anything, wait_until: nil, + job_name: 'GetUspsProofingResultsJob', ) end end @@ -705,6 +722,7 @@ fraud_suspected: true, passed: false, reason: 'Failed status', + job_name: 'GetUspsProofingResultsJob', ), ) expect(job_analytics).to have_logged_event( @@ -714,6 +732,7 @@ service_provider: anything, timestamp: anything, wait_until: nil, + job_name: 'GetUspsProofingResultsJob', ) end end @@ -741,6 +760,7 @@ hash_including( passed: false, reason: 'Unsupported ID type', + job_name: 'GetUspsProofingResultsJob', ), ) @@ -751,6 +771,7 @@ service_provider: anything, timestamp: anything, wait_until: nil, + job_name: 'GetUspsProofingResultsJob', ) end end @@ -779,6 +800,7 @@ reason: 'Enrollment has expired', transaction_end_date_time: nil, transaction_start_date_time: nil, + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -799,6 +821,7 @@ 'GetUspsProofingResultsJob: Enrollment incomplete', hash_including( response_message: 'More than 30 days have passed since opt-in to IPP', + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -830,12 +853,16 @@ hash_including( passed: false, reason: 'Enrollment has expired', + job_name: 'GetUspsProofingResultsJob', ), ) expect(job_analytics).to have_logged_event( 'GetUspsProofingResultsJob: Unexpected response received', - hash_including(reason: 'Unexpected number of days before enrollment expired'), + hash_including( + reason: 'Unexpected number of days before enrollment expired', + ), + job_name: 'GetUspsProofingResultsJob', ) end end @@ -860,6 +887,7 @@ hash_including( reason: 'Invalid enrollment code', response_message: /Enrollment code [0-9]{16} does not exist/, + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -884,6 +912,7 @@ hash_including( reason: 'Invalid applicant unique id', response_message: /Applicant [0-9a-z]{18} does not exist/, + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -920,6 +949,7 @@ 'GetUspsProofingResultsJob: Exception raised', hash_including( status: 'Not supported', + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -1041,6 +1071,7 @@ 'GetUspsProofingResultsJob: Enrollment incomplete', hash_including( response_message: 'Customer has not been to a post office to complete IPP', + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -1106,6 +1137,7 @@ hash_including( passed: false, reason: 'Provided secondary proof of address', + job_name: 'GetUspsProofingResultsJob', ), ) end @@ -1125,5 +1157,20 @@ job.perform Time.zone.now end end + + describe 'IPP Enrollments Ready Job Enabled' do + before do + allow(IdentityConfig.store).to( + receive(:in_person_enrollments_ready_job_enabled).and_return(true), + ) + allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false) + end + + it 'does not request any enrollment records' do + # no stubbing means this test will fail if the UspsInPersonProofing::Proofer + # tries to connect to the USPS API + job.perform Time.zone.now + end + end end end diff --git a/spec/jobs/get_usps_ready_proofing_results_job_spec.rb b/spec/jobs/get_usps_ready_proofing_results_job_spec.rb new file mode 100644 index 00000000000..e73f034c826 --- /dev/null +++ b/spec/jobs/get_usps_ready_proofing_results_job_spec.rb @@ -0,0 +1,287 @@ +require 'rails_helper' + +RSpec.describe GetUspsReadyProofingResultsJob do + include UspsIppHelper + include ApproximatingHelper + + let(:reprocess_delay_minutes) { 2.0 } + let(:request_delay_ms) { 0 } + let(:job) { GetUspsReadyProofingResultsJob.new } + let(:job_analytics) { FakeAnalytics.new } + let(:transaction_start_date_time) do + ActiveSupport::TimeZone[-6].strptime( + '12/17/2020 033855', + '%m/%d/%Y %H%M%S', + ).in_time_zone('UTC') + end + let(:transaction_end_date_time) do + ActiveSupport::TimeZone[-6].strptime( + '12/17/2020 034055', + '%m/%d/%Y %H%M%S', + ).in_time_zone('UTC') + end + + before do + allow(Rails).to receive(:cache).and_return( + ActiveSupport::Cache::RedisCacheStore.new(url: IdentityConfig.store.redis_throttle_url), + ) + ActiveJob::Base.queue_adapter = :test + allow(job).to receive(:analytics).and_return(job_analytics) + allow(IdentityConfig.store).to receive(:get_usps_proofing_results_job_reprocess_delay_minutes). + and_return(reprocess_delay_minutes) + stub_const( + 'GetUspsProofingResultsJob::REQUEST_DELAY_IN_SECONDS', + request_delay_ms / GetUspsProofingResultsJob::MILLISECONDS_PER_SECOND, + ) + stub_request_token + if respond_to?(:pending_enrollment) + pending_enrollment.update(enrollment_established_at: 3.days.ago) + end + end + + describe '#perform' do + describe 'IPP enabled' do + describe 'Ready Job enabled' do + let!(:pending_enrollments) do + [ + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'BALTIMORE' }, + issuer: 'http://localhost:3000', + ready_for_status_check: true + ), + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'FRIENDSHIP' }, + ready_for_status_check: true + ), + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'WASHINGTON' }, + ready_for_status_check: true + ), + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'ARLINGTON' }, + ready_for_status_check: true + ), + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'DEANWOOD' }, + ready_for_status_check: true + ), + ] + end + let(:pending_enrollment) { pending_enrollments[0] } + + before do + allow(InPersonEnrollment).to receive(:needs_status_check_on_ready_enrollments). + and_return([pending_enrollment]) + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to( + receive(:in_person_enrollments_ready_job_enabled).and_return(true), + ) + end + + it 'requests the enrollments that need their status checked' do + stub_request_passed_proofing_results + + freeze_time do + job.perform(Time.zone.now) + + expect(InPersonEnrollment).to( + have_received(:needs_status_check_on_ready_enrollments). + with(...reprocess_delay_minutes.minutes.ago), + ) + end + end + + it 'records the last attempted status check regardless of response code and contents' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_ready_enrollments). + and_return(pending_enrollments) + stub_request_proofing_results_with_responses( + request_failed_proofing_results_args, + request_in_progress_proofing_results_args, + request_in_progress_proofing_results_args, + request_failed_proofing_results_args, + ) + + expect(pending_enrollments.pluck(:status_check_attempted_at)).to( + all(eq nil), + 'failed test precondition: + pending enrollments must not set status check attempted time', + ) + + expect(pending_enrollments.pluck(:status_check_completed_at)).to( + all(eq nil), + 'failed test precondition: + pending enrollments must not set status check completed time', + ) + + freeze_time do + job.perform(Time.zone.now) + + expect( + pending_enrollments. + map(&:reload). + pluck(:status_check_attempted_at), + ).to( + all(eq Time.zone.now), + 'job must update status check attempted time for all pending enrollments', + ) + + expect( + pending_enrollments. + map(&:reload). + pluck(:status_check_completed_at), + ).to( + all(eq Time.zone.now), + 'job must update status check completed time for all pending enrollments', + ) + end + end + + it 'logs a message when the job starts' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_ready_enrollments). + and_return(pending_enrollments) + stub_request_proofing_results_with_responses( + request_failed_proofing_results_args, + request_in_progress_proofing_results_args, + request_in_progress_proofing_results_args, + request_failed_proofing_results_args, + ) + + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Job started', + enrollments_count: 5, + reprocess_delay_minutes: 2.0, + job_name: 'GetUspsReadyProofingResultsJob', + ) + end + + it 'logs a message with counts of various outcomes when the job completes (errored > 0)' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_ready_enrollments). + and_return(pending_enrollments) + stub_request_proofing_results_with_responses( + request_passed_proofing_results_args, + request_in_progress_proofing_results_args, + { status: 500 }, + request_failed_proofing_results_args, + request_expired_proofing_results_args, + ) + + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Job completed', + duration_seconds: anything, + enrollments_checked: 5, + enrollments_errored: 1, + enrollments_expired: 1, + enrollments_failed: 1, + enrollments_in_progress: 1, + enrollments_passed: 1, + percent_enrollments_errored: 20.00, + job_name: 'GetUspsReadyProofingResultsJob', + ) + + expect( + job_analytics.events['GetUspsProofingResultsJob: Job completed']. + first[:duration_seconds], + ).to be >= 0.0 + end + + it 'logs a message with counts of various outcomes when the job completes (errored = 0)' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_ready_enrollments). + and_return(pending_enrollments) + stub_request_proofing_results_with_responses( + request_passed_proofing_results_args, + ) + + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Job completed', + duration_seconds: anything, + enrollments_checked: 5, + enrollments_errored: 0, + enrollments_expired: 0, + enrollments_failed: 0, + enrollments_in_progress: 0, + enrollments_passed: 5, + percent_enrollments_errored: 0.00, + job_name: 'GetUspsReadyProofingResultsJob', + ) + + expect( + job_analytics.events['GetUspsProofingResultsJob: Job completed']. + first[:duration_seconds], + ).to be >= 0.0 + end + + it 'logs a message with counts of various outcomes when the job completes + (no enrollments)' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_ready_enrollments). + and_return([]) + stub_request_proofing_results_with_responses( + request_passed_proofing_results_args, + ) + + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Job completed', + duration_seconds: anything, + enrollments_checked: 0, + enrollments_errored: 0, + enrollments_expired: 0, + enrollments_failed: 0, + enrollments_in_progress: 0, + enrollments_passed: 0, + percent_enrollments_errored: 0.00, + job_name: 'GetUspsReadyProofingResultsJob', + ) + + expect( + job_analytics.events['GetUspsProofingResultsJob: Job completed']. + first[:duration_seconds], + ).to be >= 0.0 + end + end + end + + describe 'IPP disabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(false) + allow(IdentityConfig.store).to( + receive(:in_person_enrollments_ready_job_enabled).and_return(true), + ) + allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false) + end + + it 'does not request any enrollment records' do + # no stubbing means this test will fail if the UspsInPersonProofing::Proofer + # tries to connect to the USPS API + job.perform Time.zone.now + end + end + + describe 'Ready Job disabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to( + receive(:in_person_enrollments_ready_job_enabled).and_return(false), + ) + allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false) + end + + it 'does not request any enrollment records' do + # no stubbing means this test will fail if the UspsInPersonProofing::Proofer + # tries to connect to the USPS API + job.perform Time.zone.now + end + end + end +end diff --git a/spec/jobs/get_usps_waiting_proofing_results_job_spec.rb b/spec/jobs/get_usps_waiting_proofing_results_job_spec.rb new file mode 100644 index 00000000000..2f21550a9cb --- /dev/null +++ b/spec/jobs/get_usps_waiting_proofing_results_job_spec.rb @@ -0,0 +1,287 @@ +require 'rails_helper' + +RSpec.describe GetUspsWaitingProofingResultsJob do + include UspsIppHelper + include ApproximatingHelper + + let(:reprocess_delay_minutes) { 2.0 } + let(:request_delay_ms) { 0 } + let(:job) { GetUspsWaitingProofingResultsJob.new } + let(:job_analytics) { FakeAnalytics.new } + let(:transaction_start_date_time) do + ActiveSupport::TimeZone[-6].strptime( + '12/17/2020 033855', + '%m/%d/%Y %H%M%S', + ).in_time_zone('UTC') + end + let(:transaction_end_date_time) do + ActiveSupport::TimeZone[-6].strptime( + '12/17/2020 034055', + '%m/%d/%Y %H%M%S', + ).in_time_zone('UTC') + end + + before do + allow(Rails).to receive(:cache).and_return( + ActiveSupport::Cache::RedisCacheStore.new(url: IdentityConfig.store.redis_throttle_url), + ) + ActiveJob::Base.queue_adapter = :test + allow(job).to receive(:analytics).and_return(job_analytics) + allow(IdentityConfig.store).to receive(:get_usps_proofing_results_job_reprocess_delay_minutes). + and_return(reprocess_delay_minutes) + stub_const( + 'GetUspsProofingResultsJob::REQUEST_DELAY_IN_SECONDS', + request_delay_ms / GetUspsProofingResultsJob::MILLISECONDS_PER_SECOND, + ) + stub_request_token + if respond_to?(:pending_enrollment) + pending_enrollment.update(enrollment_established_at: 3.days.ago) + end + end + + describe '#perform' do + describe 'IPP enabled' do + describe 'Ready Job enabled' do + let!(:pending_enrollments) do + [ + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'BALTIMORE' }, + issuer: 'http://localhost:3000', + ready_for_status_check: false + ), + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'FRIENDSHIP' }, + ready_for_status_check: false + ), + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'WASHINGTON' }, + ready_for_status_check: false + ), + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'ARLINGTON' }, + ready_for_status_check: false + ), + create( + :in_person_enrollment, :pending, + selected_location_details: { name: 'DEANWOOD' }, + ready_for_status_check: false + ), + ] + end + let(:pending_enrollment) { pending_enrollments[0] } + + before do + allow(InPersonEnrollment).to receive(:needs_status_check_on_waiting_enrollments). + and_return([pending_enrollment]) + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to( + receive(:in_person_enrollments_ready_job_enabled).and_return(true), + ) + end + + it 'requests the enrollments that need their status checked' do + stub_request_passed_proofing_results + + freeze_time do + job.perform(Time.zone.now) + + expect(InPersonEnrollment).to( + have_received(:needs_status_check_on_waiting_enrollments). + with(...reprocess_delay_minutes.minutes.ago), + ) + end + end + + it 'records the last attempted status check regardless of response code and contents' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_waiting_enrollments). + and_return(pending_enrollments) + stub_request_proofing_results_with_responses( + request_failed_proofing_results_args, + request_in_progress_proofing_results_args, + request_in_progress_proofing_results_args, + request_failed_proofing_results_args, + ) + + expect(pending_enrollments.pluck(:status_check_attempted_at)).to( + all(eq nil), + 'failed test precondition: + pending enrollments must not set status check attempted time', + ) + + expect(pending_enrollments.pluck(:status_check_completed_at)).to( + all(eq nil), + 'failed test precondition: + pending enrollments must not set status check completed time', + ) + + freeze_time do + job.perform(Time.zone.now) + + expect( + pending_enrollments. + map(&:reload). + pluck(:status_check_attempted_at), + ).to( + all(eq Time.zone.now), + 'job must update status check attempted time for all pending enrollments', + ) + + expect( + pending_enrollments. + map(&:reload). + pluck(:status_check_completed_at), + ).to( + all(eq Time.zone.now), + 'job must update status check completed time for all pending enrollments', + ) + end + end + + it 'logs a message when the job starts' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_waiting_enrollments). + and_return(pending_enrollments) + stub_request_proofing_results_with_responses( + request_failed_proofing_results_args, + request_in_progress_proofing_results_args, + request_in_progress_proofing_results_args, + request_failed_proofing_results_args, + ) + + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Job started', + enrollments_count: 5, + reprocess_delay_minutes: 2.0, + job_name: 'GetUspsWaitingProofingResultsJob', + ) + end + + it 'logs a message with counts of various outcomes when the job completes (errored > 0)' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_waiting_enrollments). + and_return(pending_enrollments) + stub_request_proofing_results_with_responses( + request_passed_proofing_results_args, + request_in_progress_proofing_results_args, + { status: 500 }, + request_failed_proofing_results_args, + request_expired_proofing_results_args, + ) + + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Job completed', + duration_seconds: anything, + enrollments_checked: 5, + enrollments_errored: 1, + enrollments_expired: 1, + enrollments_failed: 1, + enrollments_in_progress: 1, + enrollments_passed: 1, + percent_enrollments_errored: 20.00, + job_name: 'GetUspsWaitingProofingResultsJob', + ) + + expect( + job_analytics.events['GetUspsProofingResultsJob: Job completed']. + first[:duration_seconds], + ).to be >= 0.0 + end + + it 'logs a message with counts of various outcomes when the job completes (errored = 0)' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_waiting_enrollments). + and_return(pending_enrollments) + stub_request_proofing_results_with_responses( + request_passed_proofing_results_args, + ) + + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Job completed', + duration_seconds: anything, + enrollments_checked: 5, + enrollments_errored: 0, + enrollments_expired: 0, + enrollments_failed: 0, + enrollments_in_progress: 0, + enrollments_passed: 5, + percent_enrollments_errored: 0.00, + job_name: 'GetUspsWaitingProofingResultsJob', + ) + + expect( + job_analytics.events['GetUspsProofingResultsJob: Job completed']. + first[:duration_seconds], + ).to be >= 0.0 + end + + it 'logs a message with counts of various outcomes when the job completes + (no enrollments)' do + allow(InPersonEnrollment).to receive(:needs_status_check_on_waiting_enrollments). + and_return([]) + stub_request_proofing_results_with_responses( + request_passed_proofing_results_args, + ) + + job.perform(Time.zone.now) + + expect(job_analytics).to have_logged_event( + 'GetUspsProofingResultsJob: Job completed', + duration_seconds: anything, + enrollments_checked: 0, + enrollments_errored: 0, + enrollments_expired: 0, + enrollments_failed: 0, + enrollments_in_progress: 0, + enrollments_passed: 0, + percent_enrollments_errored: 0.00, + job_name: 'GetUspsWaitingProofingResultsJob', + ) + + expect( + job_analytics.events['GetUspsProofingResultsJob: Job completed']. + first[:duration_seconds], + ).to be >= 0.0 + end + end + end + + describe 'IPP disabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(false) + allow(IdentityConfig.store).to( + receive(:in_person_enrollments_ready_job_enabled).and_return(true), + ) + allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false) + end + + it 'does not request any enrollment records' do + # no stubbing means this test will fail if the UspsInPersonProofing::Proofer + # tries to connect to the USPS API + job.perform Time.zone.now + end + end + + describe 'Ready Job disabled' do + before do + allow(IdentityConfig.store).to receive(:in_person_proofing_enabled).and_return(true) + allow(IdentityConfig.store).to( + receive(:in_person_enrollments_ready_job_enabled).and_return(false), + ) + allow(IdentityConfig.store).to receive(:usps_mock_fallback).and_return(false) + end + + it 'does not request any enrollment records' do + # no stubbing means this test will fail if the UspsInPersonProofing::Proofer + # tries to connect to the USPS API + job.perform Time.zone.now + end + end + end +end diff --git a/spec/models/in_person_enrollment_spec.rb b/spec/models/in_person_enrollment_spec.rb index 2098b5662b7..02e8da27762 100644 --- a/spec/models/in_person_enrollment_spec.rb +++ b/spec/models/in_person_enrollment_spec.rb @@ -180,6 +180,115 @@ end end + describe 'status checks for ready and waiting enrollments' do + let(:check_interval) { ...1.hour.ago } + let!(:passed_enrollment) do + create(:in_person_enrollment, :passed, ready_for_status_check: true) + end + let!(:failing_enrollment) do + create(:in_person_enrollment, :failed, ready_for_status_check: true) + end + let!(:expired_enrollment) do + create(:in_person_enrollment, :expired, ready_for_status_check: true) + end + let!(:checked_pending_enrollment) do + create( + :in_person_enrollment, :pending, status_check_attempted_at: Time.zone.now, + ready_for_status_check: true + ) + end + let!(:ready_enrollments) do + [ + create(:in_person_enrollment, :pending, ready_for_status_check: true), + create(:in_person_enrollment, :pending, ready_for_status_check: true), + create(:in_person_enrollment, :pending, ready_for_status_check: true), + create(:in_person_enrollment, :pending, ready_for_status_check: true), + ] + end + let!(:needy_enrollments) do + [ + create(:in_person_enrollment, :pending, ready_for_status_check: false), + create(:in_person_enrollment, :pending, ready_for_status_check: false), + create(:in_person_enrollment, :pending, ready_for_status_check: false), + create(:in_person_enrollment, :pending, ready_for_status_check: false), + ] + end + + it 'returns only pending enrollments that are ready for status check' do + expect(InPersonEnrollment.count).to eq(12) + ready_results = InPersonEnrollment.needs_status_check_on_ready_enrollments(check_interval) + expect(ready_results.length).to eq ready_enrollments.length + expect(ready_results.pluck(:id)).to match_array ready_enrollments.pluck(:id) + expect(ready_results.pluck(:id)).not_to match_array needy_enrollments.pluck(:id) + ready_results.each do |result| + expect(result.pending?).to be_truthy + end + end + + it 'indicates whether a ready enrollment needs a status check' do + expect(passed_enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( + be(false), + ) + expect(failing_enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( + be(false), + ) + expect(expired_enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( + be(false), + ) + expect(checked_pending_enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( + be(false), + ) + needy_enrollments.each do |enrollment| + expect(enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( + be(false), + ) + end + ready_enrollments.each do |enrollment| + expect(enrollment.needs_status_check_on_ready_enrollment?(check_interval)).to( + be(true), + ) + end + end + + it 'returns only pending enrollments that are not ready for status check' do + expect(InPersonEnrollment.count).to eq(12) + waiting_results = InPersonEnrollment.needs_status_check_on_waiting_enrollments(check_interval) + expect(waiting_results.length).to eq needy_enrollments.length + expect(waiting_results.pluck(:id)).to match_array needy_enrollments.pluck(:id) + expect(waiting_results.pluck(:id)).not_to match_array ready_enrollments.pluck(:id) + waiting_results.each do |result| + expect(result.pending?).to be_truthy + end + end + + it 'indicates whether a waiting enrollment needs a status check' do + expect(passed_enrollment.needs_status_check_on_waiting_enrollment?(check_interval)).to( + be(false), + ) + expect( + failing_enrollment.needs_status_check_on_waiting_enrollment?(check_interval), + ).to( + be(false), + ) + expect( + expired_enrollment.needs_status_check_on_waiting_enrollment?(check_interval), + ).to( + be(false), + ) + expect( + checked_pending_enrollment.needs_status_check_on_waiting_enrollment?(check_interval), + ).to( + be(false), + ) + needy_enrollments.each do |enrollment| + expect(enrollment.needs_status_check_on_waiting_enrollment?(check_interval)).to be(true) + end + ready_enrollments.each do |enrollment| + expect(enrollment.needs_status_check_on_waiting_enrollment?(check_interval)).to be(false) + end + end + end + describe 'minutes_since_established' do let(:enrollment) do create( From 24a492f23274269f09351a531598014280f86d3f Mon Sep 17 00:00:00 2001 From: Alex Bradley Date: Tue, 6 Jun 2023 09:11:05 -0400 Subject: [PATCH 18/20] LG-9858 GPO Personal Key fix (#8533) * fixed users under fraud in gpo not seeing personal key * add tests to check confirm_profile method * add changelog changelog: Bug Fixes, IdV personal key, allow profiles to reach personal key step from confirm_profile_has_been_created --- .../idv/personal_key_controller.rb | 2 +- .../idv/personal_key_controller_spec.rb | 95 ++++++++----------- 2 files changed, 40 insertions(+), 57 deletions(-) diff --git a/app/controllers/idv/personal_key_controller.rb b/app/controllers/idv/personal_key_controller.rb index e00ec967e05..e58df4ce1f0 100644 --- a/app/controllers/idv/personal_key_controller.rb +++ b/app/controllers/idv/personal_key_controller.rb @@ -74,7 +74,7 @@ def personal_key def profile return idv_session.profile if idv_session.profile - current_user.active_profile + current_user.active_or_pending_profile end def generate_personal_key diff --git a/spec/controllers/idv/personal_key_controller_spec.rb b/spec/controllers/idv/personal_key_controller_spec.rb index abfcd6748c6..c78f25b0b28 100644 --- a/spec/controllers/idv/personal_key_controller_spec.rb +++ b/spec/controllers/idv/personal_key_controller_spec.rb @@ -87,6 +87,38 @@ def index expect(response).to redirect_to account_path end end + + context 'profile is pending from a different session' do + context 'profile is pending due to fraud review' do + before do + profile.deactivate_for_fraud_review + subject.idv_session.profile_id = nil + end + + it 'does not redirect' do + get :index + + expect(profile.user.pending_profile?).to eq true + expect(profile.fraud_review_pending_at).to_not eq nil + expect(response).to_not be_redirect + end + end + + context 'profile is pending due to in person proofing' do + before do + profile.update!(deactivation_reason: :in_person_verification_pending) + subject.idv_session.profile_id = nil + end + + it 'does not redirect' do + get :index + + expect(profile.user.pending_profile?).to eq true + expect(profile.deactivation_reason).to eq('in_person_verification_pending') + expect(response).to_not be_redirect + end + end + end end end @@ -200,12 +232,10 @@ def index subject.idv_session.create_profile_from_applicant_with_password(password) end - context 'with gpo personal key after verification' do - it 'redirects to doc auth url' do - patch :update + it 'redirects to doc auth url' do + patch :update - expect(response).to redirect_to idv_doc_auth_url - end + expect(response).to redirect_to idv_doc_auth_url end end @@ -239,39 +269,17 @@ def index context 'with device profiling decisioning enabled' do before do - ProofingComponent.create(user: user, threatmetrix: true, threatmetrix_review_status: nil) allow(IdentityConfig.store).to receive(:proofing_device_profiling).and_return(:enabled) end - context 'threatmetrix review status is nil' do + context 'fraud_review_pending_at is nil' do it 'redirects to account path' do patch :update + expect(profile.fraud_review_pending_at).to eq nil expect(response).to redirect_to account_path end - it 'logs key submitted event' do - patch :update - - expect(@analytics).to have_logged_event( - 'IdV: personal key submitted', - address_verification_method: nil, - fraud_review_pending: false, - fraud_rejection: false, - deactivation_reason: nil, - proofing_components: nil, - ) - end - end - context 'device profiling passes' do - before do - ProofingComponent.find_by(user: user).update(threatmetrix_review_status: 'pass') - end - it 'redirects to account path' do - patch :update - - expect(response).to redirect_to account_path - end it 'logs key submitted event' do patch :update @@ -286,39 +294,14 @@ def index end end - context 'device profiling gets sent to review' do - before do - ProofingComponent.find_by(user: user).update(threatmetrix_review_status: 'review') - profile.deactivate_for_fraud_review - end - - it 'redirects to idv please call path' do - patch :update - expect(response).to redirect_to idv_please_call_path - end - - it 'logs key submitted event' do - patch :update - - expect(@analytics).to have_logged_event( - 'IdV: personal key submitted', - fraud_review_pending: true, - fraud_rejection: false, - address_verification_method: nil, - deactivation_reason: nil, - proofing_components: nil, - ) - end - end - - context 'device profiling fails' do + context 'profile is in fraud_review' do before do - ProofingComponent.find_by(user: user).update(threatmetrix_review_status: 'reject') profile.deactivate_for_fraud_review end it 'redirects to idv please call path' do patch :update + expect(profile.fraud_review_pending_at).to_not eq nil expect(response).to redirect_to idv_please_call_path end From 3014caf306f5ce27fb0bda07f167b8385e0c8a32 Mon Sep 17 00:00:00 2001 From: Eric Gade <105373963+eric-gade@users.noreply.github.com> Date: Tue, 6 Jun 2023 09:28:58 -0400 Subject: [PATCH 19/20] LG-10014 [Bugfix] Password Reset Activates Any Profile (#8537) * New helper method will do proper checks to ensure that profile can actually be activated * add tests to active_after_password_reset * add guard for password_reset deactivation_reason changelog: Bug Fixes, Profiles, Password reset profile activation bugfix --------- Co-authored-by: Alex Bradley --- app/forms/verify_password_form.rb | 3 +-- app/models/profile.rb | 9 +++++++ spec/models/profile_spec.rb | 42 +++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/app/forms/verify_password_form.rb b/app/forms/verify_password_form.rb index d894f187dc2..e7457a9a908 100644 --- a/app/forms/verify_password_form.rb +++ b/app/forms/verify_password_form.rb @@ -34,8 +34,7 @@ def valid_password? def reencrypt_pii personal_key = profile.encrypt_pii(decrypted_pii, password) - profile.update(deactivation_reason: nil, active: true) - profile.save! + profile.activate_after_password_reset personal_key end diff --git a/app/models/profile.rb b/app/models/profile.rb index 3c00b3ac313..d113a180ec1 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -92,6 +92,15 @@ def activate_after_passing_in_person activate end + def activate_after_password_reset + if password_reset? + update!( + deactivation_reason: nil, + ) + activate + end + end + def deactivate(reason) update!(active: false, deactivation_reason: reason) end diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index 39a1d7b51c6..17af2a7c199 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -319,6 +319,48 @@ end end + describe '#activate_after_password_reset' do + it 'activates a profile after password reset' do + profile = create( + :profile, + user: user, + active: false, + deactivation_reason: :password_reset, + ) + + profile.activate_after_password_reset + + expect(profile.active).to eq true + expect(profile.deactivation_reason).to eq nil + end + + it 'does not activate a profile if it has a pending reason' do + profile = create( + :profile, + user: user, + active: false, + deactivation_reason: :password_reset, + fraud_review_pending_at: 1.day.ago, + ) + + expect { profile.activate_after_password_reset }.to raise_error + end + + it 'does not activate a profile with non password_reset deactivation_reason' do + profile = create( + :profile, + user: user, + active: false, + deactivation_reason: :encryption_error, + ) + + profile.activate_after_password_reset + + expect(profile.active).to eq false + expect(profile.deactivation_reason).to_not eq nil + end + end + describe '#activate_after_passing_review' do it 'activates a profile if it passes fraud review' do profile = create( From 4ea3602241e421baf403b3db2b9c252dafd91b5e Mon Sep 17 00:00:00 2001 From: Sonia Connolly Date: Tue, 6 Jun 2023 08:57:25 -0700 Subject: [PATCH 20/20] Return to VerifyInfo when attempting to navigate to earlier pages (#8535) * From VerifyInfo, navigating to earlier pages returns here Previously, users landed on the SSN page because it allows updates from VerifyInfo and it was not checking the referer. [skip changelog] * Change redo document capture spec to match new behavior - expect to land on VerifyInfo, not SSN - expect success message --- app/controllers/idv/ssn_controller.rb | 8 ++++++ spec/controllers/idv/ssn_controller_spec.rb | 28 ++++++++++++++++++- .../redo_document_capture_action_spec.rb | 10 ++----- .../idv/doc_auth/verify_info_step_spec.rb | 4 +++ 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/app/controllers/idv/ssn_controller.rb b/app/controllers/idv/ssn_controller.rb index bc53eae9798..bc45e21e254 100644 --- a/app/controllers/idv/ssn_controller.rb +++ b/app/controllers/idv/ssn_controller.rb @@ -9,6 +9,7 @@ class SsnController < ApplicationController before_action :confirm_verify_info_step_needed before_action :confirm_document_capture_complete + before_action :confirm_repeat_ssn, only: :show before_action :override_csp_for_threat_metrix_no_fsm attr_accessor :error_message @@ -50,6 +51,13 @@ def update private + def confirm_repeat_ssn + return if !pii_from_doc[:ssn] + return if request.referer == idv_verify_info_url + + redirect_to idv_verify_info_url + end + def next_url if pii_from_doc[:state] == 'PR' idv_address_url diff --git a/spec/controllers/idv/ssn_controller_spec.rb b/spec/controllers/idv/ssn_controller_spec.rb index 31ada7f5c31..1ac5cfd11fb 100644 --- a/spec/controllers/idv/ssn_controller_spec.rb +++ b/spec/controllers/idv/ssn_controller_spec.rb @@ -10,6 +10,8 @@ :flow_path => 'standard' } end + let(:ssn) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN[:ssn] } + let(:user) { create(:user) } before do @@ -84,6 +86,31 @@ end end + context 'with an ssn in session' do + let(:referer) { idv_document_capture_url } + before do + flow_session['pii_from_doc'][:ssn] = ssn + request.env['HTTP_REFERER'] = referer + end + + context 'referer is not verify_info' do + it 'redirects to verify_info' do + get :show + + expect(response).to redirect_to(idv_verify_info_url) + end + end + + context 'referer is verify_info' do + let(:referer) { idv_verify_info_url } + it 'does not redirect' do + get :show + + expect(response).to render_template :show + end + end + end + it 'overrides Content Security Policies for ThreatMetrix' do allow(IdentityConfig.store).to receive(:proofing_device_profiling). and_return(:enabled) @@ -108,7 +135,6 @@ describe '#update' do context 'with valid ssn' do - let(:ssn) { Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN[:ssn] } let(:params) { { doc_auth: { ssn: ssn } } } let(:analytics_name) { 'IdV: doc auth ssn submitted' } let(:analytics_args) do diff --git a/spec/features/idv/actions/redo_document_capture_action_spec.rb b/spec/features/idv/actions/redo_document_capture_action_spec.rb index bc45ad8e775..77632d1602e 100644 --- a/spec/features/idv/actions/redo_document_capture_action_spec.rb +++ b/spec/features/idv/actions/redo_document_capture_action_spec.rb @@ -32,13 +32,10 @@ DocAuth::Mock::DocAuthMockClient.reset! attach_and_submit_images - expect(current_path).to eq(idv_ssn_path) - fill_out_ssn_form_with_ssn_that_fails_resolution - click_on t('forms.buttons.submit.update') expect(current_path).to eq(idv_verify_info_path) check t('forms.ssn.show') expect(page).to have_content(DocAuthHelper::SSN_THAT_FAILS_RESOLUTION) - expect(page).not_to have_css('[role="status"]') + expect(page).to have_css('[role="status"]') # We verified your ID end it 'shows a troubleshooting option to allow the user to cancel and return to SP' do @@ -71,13 +68,10 @@ DocAuth::Mock::DocAuthMockClient.reset! attach_and_submit_images - expect(current_path).to eq(idv_ssn_path) - fill_out_ssn_form_with_ssn_that_fails_resolution - click_on t('forms.buttons.submit.update') expect(current_path).to eq(idv_verify_info_path) check t('forms.ssn.show') expect(page).to have_content(DocAuthHelper::SSN_THAT_FAILS_RESOLUTION) - expect(page).not_to have_css('[role="status"]') + expect(page).to have_css('[role="status"]') # We verified your ID end end end diff --git a/spec/features/idv/doc_auth/verify_info_step_spec.rb b/spec/features/idv/doc_auth/verify_info_step_spec.rb index f601cd5f19d..54d73395bdd 100644 --- a/spec/features/idv/doc_auth/verify_info_step_spec.rb +++ b/spec/features/idv/doc_auth/verify_info_step_spec.rb @@ -51,6 +51,10 @@ check t('forms.ssn.show') expect(page).not_to have_text(DocAuthHelper::GOOD_SSN_MASKED) expect(page).to have_text(DocAuthHelper::GOOD_SSN) + + # navigating to earlier pages returns here + visit(idv_document_capture_url) + expect(page).to have_current_path(idv_verify_info_path) end it 'allows the user to enter in a new address and displays updated info' do